From 75aa4371d8324a62e108ce537882c7f0bbffa08b Mon Sep 17 00:00:00 2001 From: Colby Smith Date: Tue, 26 May 2026 13:42:01 -0700 Subject: [PATCH 1/2] Add RMP support, utilities & examples Add support for extracting geothermal restrictions from BLM RMPs: introduce a large rmp_jurisdictions.csv dataset and register it in the jurisdictions registry. Add compass/utilities/finalize_rmp.py to compile parsed ordinance CSVs, save run metadata, format outputs (qualitative/quantitative), apply empirical adjustments, and generate a run summary. Update one-shot components to include geothermal-specific output columns (location, restriction_type, geothermal_applicability, ammendment). Include an rmp_demo example with configuration, schema, local docs template, plugin config, and a small jurisdictions.csv to demonstrate running the one-shot extraction workflow. --- compass/data/rmp_jurisdictions.csv | 2708 +++++++++++++++++ compass/plugin/one_shot/components.py | 4 + compass/utilities/finalize_rmp.py | 338 ++ compass/utilities/jurisdictions.py | 1 + examples/rmp_demo/README.md | 51 + examples/rmp_demo/jurisdictions.csv | 2 + examples/rmp_demo/one-shot/config.json5 | 41 + examples/rmp_demo/one-shot/local_docs.json5 | 11 + examples/rmp_demo/one-shot/plugin_config.yaml | 83 + examples/rmp_demo/one-shot/rmp_schema.json | 386 +++ 10 files changed, 3625 insertions(+) create mode 100644 compass/data/rmp_jurisdictions.csv create mode 100644 compass/utilities/finalize_rmp.py create mode 100644 examples/rmp_demo/README.md create mode 100644 examples/rmp_demo/jurisdictions.csv create mode 100644 examples/rmp_demo/one-shot/config.json5 create mode 100644 examples/rmp_demo/one-shot/local_docs.json5 create mode 100644 examples/rmp_demo/one-shot/plugin_config.yaml create mode 100644 examples/rmp_demo/one-shot/rmp_schema.json diff --git a/compass/data/rmp_jurisdictions.csv b/compass/data/rmp_jurisdictions.csv new file mode 100644 index 000000000..f3b402149 --- /dev/null +++ b/compass/data/rmp_jurisdictions.csv @@ -0,0 +1,2708 @@ +State,County,Subdivision,Jurisdiction Type,FIPS,Website +Nevada,Black_Rock_Desert-High_Rock_Canyon_NCA_RMP,Black_Rock_Desert-High_Rock_Canyon_NCA_Approved_RM,RMP_Resource_Management_Plan,71144567, +Nevada,Black_Rock_Desert-High_Rock_Canyon_NCA_RMP,Black_Rock_Desert-High_Rock_Canyon_NCA_Proposed_RM,RMP_Resource_Management_Plan,80726359, +Nevada,Black_Rock_Desert-High_Rock_Canyon_NCA_RMP,Federal_Register_Notices,RMP_Resource_Management_Plan,28615947, +Nevada,Carson_City_District_RMP_Revision_proposed,CCD_Draft_Resource_Management_Plan_and_Environment,RMP_Resource_Management_Plan,56793703, +Nevada,Carson_City_District_RMP_Revision_proposed,Miscellaneous_Documents,RMP_Resource_Management_Plan,24074905, +Nevada,Carson_City_District_RMP_Revision_proposed,Reports,RMP_Resource_Management_Plan,36319059, +Nevada,Carson_City_Field_Office_Consolidated_RMP,1._Carson_City_Consolidated_Resource_Management_Pl,RMP_Resource_Management_Plan,22634005, +Nevada,Carson_City_Field_Office_Consolidated_RMP,2._Amendment_to_Carson_City_RMP_BLM_and_Navy_RMP_f,RMP_Amendment,41784075, +Nevada,Carson_City_Field_Office_Consolidated_RMP,3._Amendment_to_Carson_City_RMP_Southern_Washoe_Co,RMP_Amendment,04924246, +Nevada,Carson_City_Field_Office_Consolidated_RMP,4._Amendment_to_Carson_City_RMP_North_Douglas_Coun,RMP_Amendment,31591264, +Nevada,Carson_City_Field_Office_Consolidated_RMP,5._Amendment_to_Carson_City_RMP_Alpine_County_Reso,RMP_Amendment,55520161, +Nevada,Carson_City_Field_Office_Consolidated_RMP,6._Amendment_to_Carson_City_RMP_Denton-Rawhide_Min,RMP_Amendment,75931628, +Nevada,Carson_City_Field_Office_Consolidated_RMP,7._Carson_City_District_Fire_Management_Plan_(2016,RMP_Fire_Management_Plan,44768680, +Nevada,Carson_City_Field_Office_Consolidated_RMP,8._Carson_City_District_Visual_Resource_Inventory,RMP_Visual_Resource_Inventory,18158427, +Nevada,Ely_RMP,01._Ely_Record_of_Decision_and_Approved_Resource_M,RMP_Resource_Management_Plan,97919349, +Nevada,Ely_RMP,02._Ely_Proposed_Resource_Management_Plan_and_Fina,RMP_Resource_Management_Plan,35488476, +Nevada,Ely_RMP,03._Ely_Draft_Resource_Management_Plan_and_Prelimi,RMP_Resource_Management_Plan,86276550, +Nevada,Ely_RMP,04._Amendment_to_Ely_RMP_Spring_Valley_Wind_Energy,RMP_Amendment,29067743, +Nevada,Ely_RMP,05._Amendment_to_Ely_RMP_ARMPA_ROD_for_Solar_Energ,RMP_Amendment,30618001, +Nevada,Ely_RMP,06._Ely_District_Fire_Management_Plan_(2016),RMP_Fire_Management_Plan,00071468, +Nevada,Ely_RMP,07._2011_Ely_District_Visual_Resource_Inventory_Re,RMP_Visual_Resource_Inventory,38119432, +Nevada,Las_Vegas_RMP,Amendment_to_Las_Vegas_RMP_--_Las_Vegas_Valley_Dis,RMP_Amendment,15750920, +Nevada,Las_Vegas_RMP,Amendment_to_Las_Vegas_RMP_--_Route_Designations_f,RMP_Amendment,89470944, +Nevada,Las_Vegas_RMP,Amendment_to_the_RMP_-_Silver_State_Solar_South_Pr,RMP_Amendment,75224688, +Nevada,Las_Vegas_RMP,Las_Vegas_Resource_Management_Plan_(1998),RMP_Resource_Management_Plan,48235329, +Nevada,Las_Vegas_RMP,Las_Vegas_Resource_Management_Plan_Maintenance_#1,RMP_Resource_Management_Plan,67391508, +Nevada,Las_Vegas_RMP,Las_Vegas_Resource_Management_Plan_Maintenance_#2,RMP_Resource_Management_Plan,32734564, +Nevada,Las_Vegas_RMP,Southern_Nevada_District_Visual_Resource_Inventory,RMP_Visual_Resource_Inventory,53724850, +Nevada,Las_Vegas_RMP,Updates_to_the_1998_Las_Vegas_RMP_EIS_-_(incomplet,RMP_Resource_Management_Plan,79026519, +Nevada,Nevada_Test_and_Training_Range_RMP,Draft_Nevada_Test_and_Training_Range_Resource_Mana,RMP_Resource_Management_Plan,83281212, +Nevada,Nevada_Test_and_Training_Range_RMP,Nellis_Air_Force_Range_Resource_Management_Plan_(1,RMP_Resource_Management_Plan,09896083, +Nevada,Nevada_Test_and_Training_Range_RMP,Proposed_Nevada_Test_and_Training_Range_Resource_M,RMP_Resource_Management_Plan,54449311, +Nevada,Nevada_Test_and_Training_Range_RMP,Record_of_Decision_for_the_Approved_Nevada_Test_an,RMP_Resource_Management_Plan,53118822, +Nevada,Red_Rock_Canyon_NCA_RMP,Proposed_General_Management_Plan_and_Final_Environ,RMP_Resource_Management_Plan,78209323, +Nevada,Red_Rock_Canyon_NCA_RMP,Red_Rock_Canyon_NCA_Resource_Management_Plan_and_R,RMP_Resource_Management_Plan,64909313, +Nevada,Shoshone-Eureka_RMP,2011_Battle_Mountain_District_Visual_Resource_Inve,RMP_Resource_Management_Plan,50815920, +Nevada,Shoshone-Eureka_RMP,Unknown,RMP_Resource_Management_Plan,92540541, +Nevada,Sloan_Canyon_NCA_RMP,1._Plan_Maintenance_Action_to_the_Sloan_Canyon_NCA,RMP_Resource_Management_Plan,87263525, +Nevada,Sloan_Canyon_NCA_RMP,2._Sloan_Canyon_NCA_Proposed_RMP_and_Final_EIS_(20,RMP_Resource_Management_Plan,13035976, +Nevada,Sloan_Canyon_NCA_RMP,3._Sloan_Canyon_NCA_ROD_for_the_ARMP_FEIS_and_Appr,RMP_Resource_Management_Plan,53830029, +Nevada,Tonopah_RMP,2011_Battle_Mountain_District_Visual_Resources_Inv,RMP_Resource_Management_Plan,39611902, +Nevada,Tonopah_RMP,Unknown,RMP_Resource_Management_Plan,10393130, +Nevada,Wells_RMP,2012_Elko_District_Visual_Resource_Inventory_Repor,RMP_Visual_Resource_Inventory,41163791, +Nevada,Wells_RMP,Amendments_to_the_Wells_RMP_and_Elko_RMP,RMP_Amendment,10458377, +Nevada,Wells_RMP,Unknown,RMP_Resource_Management_Plan,52448542, +Nevada,Winnemucca_District_RMP,0._Plan_Maintenance,RMP_Resource_Management_Plan,26856004, +Nevada,Winnemucca_District_RMP,1._ROD,RMP_Resource_Management_Plan,36606231, +Nevada,Winnemucca_District_RMP,2._Proposed_RMP_Final_EIS_-_by_Chapter,RMP_Resource_Management_Plan,86211614, +Nevada,Winnemucca_District_RMP,3._Proposed_RMP_Final_EIS_-_by_Volume,RMP_Resource_Management_Plan,84138292, +Nevada,Winnemucca_District_RMP,4._Appendices,RMP_Resource_Management_Plan,39321226, +Nevada,Winnemucca_District_RMP,5._Reports,RMP_Resource_Management_Plan,51761980, +Utah,Price_RMP,Background_Documents,RMP_Resource_Management_Plan,18420908, +Utah,Price_RMP,Bulletins,RMP_Resource_Management_Plan,76281491, +Utah,Price_RMP,Draft_Resource_Management_Plan_and_Draft_Environme,RMP_Resource_Management_Plan,73309526, +Utah,Price_RMP,Maintenance_Action_-_Nelson_Mountain_acreage_to_US,RMP_Resource_Management_Plan,61214691, +Utah,Price_RMP,Maintenance_Actions_Adjusting_Gordon_Creek_Wildlif,RMP_Resource_Management_Plan,44276278, +Utah,Price_RMP,Maintenance_Actions_Adjusting_Stipulations_for_Sur,RMP_Resource_Management_Plan,88767693, +Utah,Price_RMP,Maintenance_Actions_Adjusting_White-tailed_Prairie,RMP_Resource_Management_Plan,08980193, +Utah,Price_RMP,Maps_-_Proposed_Resource_Management_Plan_Final_Env,RMP_Resource_Management_Plan,90402463, +Utah,Price_RMP,Mineral_Potential_Report_and_Maps,RMP_Resource_Management_Plan,64258158, +Utah,Price_RMP,Proposed_Resource_Management_Plan_Final_Environmen,RMP_Resource_Management_Plan,12066463, +Utah,Price_RMP,Record_of_Decision_Approved_Resource_Management_Pl,RMP_Resource_Management_Plan,16053208, +Utah,Price_RMP,Scoping_Report,RMP_Resource_Management_Plan,64159722, +Utah,Price_RMP,Supplemental_Draft_Resource_Management_Plan_Enviro,RMP_Resource_Management_Plan,34538018, +Utah,Warm_Springs_Resource_Area_Approved_RMP_ROD,Draft_Resource_Management_Plan_Draft_Environmental,RMP_Resource_Management_Plan,72124876, +Utah,Warm_Springs_Resource_Area_Approved_RMP_ROD,Proposed_Resource_Management_Plan_Final_Environmen,RMP_Resource_Management_Plan,67660583, +Utah,Warm_Springs_Resource_Area_Approved_RMP_ROD,Record_of_Decision_Approved_Resource_Management_Pl,RMP_Resource_Management_Plan,27205657, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, +,,,,, diff --git a/compass/plugin/one_shot/components.py b/compass/plugin/one_shot/components.py index d2ea16aa8..455c0380f 100644 --- a/compass/plugin/one_shot/components.py +++ b/compass/plugin/one_shot/components.py @@ -404,8 +404,12 @@ def _to_dataframe(self, data): full_df = full_df.merge(df, on="feature", how="left") possible_out_cols = [ + "location", + "restriction_type", + "geothermal_applicability", "value", "units", + "ammendment", "summary", "year", "section", diff --git a/compass/utilities/finalize_rmp.py b/compass/utilities/finalize_rmp.py new file mode 100644 index 000000000..1d9aae81b --- /dev/null +++ b/compass/utilities/finalize_rmp.py @@ -0,0 +1,338 @@ +"""COMPASS utilities for finalizing a run directory""" + +import json +import getpass +import logging +from pathlib import Path + +import pandas as pd +from elm.version import __version__ as elm_version + +from compass import __version__ as compass_version +from compass.utilities.parsing import ( + extract_ord_year_from_doc_attrs, + num_ordinances_dataframe, + ordinances_bool_index, +) + + +logger = logging.getLogger(__name__) +_PARSED_COLS = [ + # TODO: Put these in an enum + "county", + "state", + "subdivision", + "jurisdiction_type", + "FIPS", + "feature", + "location", + "restriction_type", + "geothermal_applicability", + "value", + "units", + "section", + "summary", + "year", + "source", + "quantitative", +] +QUANT_OUT_COLS = _PARSED_COLS[:-1] +"""Output columns in quantitative ordinance file""" +QUAL_OUT_COLS = _PARSED_COLS[:6] + _PARSED_COLS[-5:-1] +"""Output columns in qualitative ordinance file""" + + +def save_run_meta( + dirs, + tech, + start_date, + end_date, + num_jurisdictions_searched, + num_jurisdictions_found, + total_cost, + models, +): + """Persist metadata describing an ordinance collection run + + Parameters + ---------- + dirs : compass.utilities.base.Directories + Directory container describing where outputs, logs, and working + files should be written during the run. + tech : {"wind", "solar", "small wind"} + Technology targeted by the collection run. The value is stored + verbatim in the metadata file for downstream reporting. + start_date : datetime.datetime + Timestamp marking when the run began. + end_date : datetime.datetime + Timestamp marking when the run finished. + num_jurisdictions_searched : int + Number of jurisdictions evaluated during the run. + num_jurisdictions_found : int + Number of jurisdictions that produced at least one ordinance. + total_cost : float + Aggregate cost incurred by LLM usage for the run. ``None`` or + zero values are recorded as ``null`` in the metadata. + models : dict + Mapping from LLM task identifiers (as str) to configuration + objects (:class:`~compass.llm.config.OpenAIConfig`) used + throughout the run. The function records a condensed summary of + each configuration. + + Returns + ------- + float + Total runtime of the collection, expressed in seconds. + + Notes + ----- + The function writes ``meta.json`` into ``dirs.out`` alongside + references to other artifacts generated during the run. The return + value mirrors the ``total_time`` entry stored in the metadata. + """ + + try: + username = getpass.getuser() + except OSError: + username = "Unknown" + + time_elapsed = end_date - start_date + meta_data = { + "username": username, + "versions": {"elm": elm_version, "compass": compass_version}, + "technology": tech, + "models": _extract_model_info_from_all_models(models), + "time_start_utc": start_date.isoformat(), + "time_end_utc": end_date.isoformat(), + "total_time": time_elapsed.seconds, + "total_time_string": str(time_elapsed), + "num_jurisdictions_searched": num_jurisdictions_searched, + "num_jurisdictions_found": num_jurisdictions_found, + "cost": total_cost or None, + "manifest": {}, + } + manifest = { + "LOG_DIR": dirs.logs, + "CLEAN_FILE_DIR": dirs.clean_files, + "JURISDICTION_DBS_DIR": dirs.jurisdiction_dbs, + "ORDINANCE_FILES_DIR": dirs.ordinance_files, + "USAGE_FILE": dirs.out / "usage.json", + "JURISDICTION_FILE": dirs.out / "jurisdictions.json", + "QUANT_DATA_FILE": dirs.out / "quantitative_ordinances.csv", + "QUAL_DATA_FILE": dirs.out / "quantitative_ordinances.csv", + } + for name, file_path in manifest.items(): + if file_path.exists(): + meta_data["manifest"][name] = str(file_path.relative_to(dirs.out)) + else: + meta_data["manifest"][name] = None + + meta_data["manifest"]["META_FILE"] = "meta.json" + with (dirs.out / "meta.json").open("w", encoding="utf-8") as fh: + json.dump(meta_data, fh, indent=4) + + return time_elapsed.seconds + + +def doc_infos_to_db(doc_infos): + """Aggregate parsed ordinance CSV files into a normalized database + + Parameters + ---------- + doc_infos : Iterable + Iterable of dictionaries describing ordinance extraction + results. Each dictionary must contain ``"ord_db_fp"`` (path to a + parsed CSV), ``"source"`` (document URL), ``"date"`` (tuple of + year, month, day, with ``None`` allowed), and ``"jurisdiction"`` + (a :class:`~compass.utilities.location.Jurisdiction` instance). + + Returns + ------- + pandas.DataFrame + Consolidated ordinance dataset. + int + Number of jurisdictions contributing at least one ordinance to + the consolidated dataset. + + Notes + ----- + Empty or ``None`` entries in ``doc_infos`` are skipped. Ordinance + CSVs that lack parsed values (``num_ordinances_dataframe`` equals + zero) are ignored. The returned DataFrame enforces an ordered column + layout and casts the ``quantitative`` flag to nullable boolean. + """ + db = [] + for doc_info in doc_infos: + if doc_info is None: + continue + + ord_db_fp = doc_info.get("ord_db_fp") + if ord_db_fp is None: + continue + + ord_db = pd.read_csv(ord_db_fp) + + if num_ordinances_dataframe(ord_db) == 0: + continue + + results = _db_results(ord_db, doc_info) + results = _formatted_db(results) + db.append(results) + + if not db: + return pd.DataFrame(columns=_PARSED_COLS), 0 + + logger.info("Compiling final database for %d jurisdiction(s)", len(db)) + num_jurisdictions_found = len(db) + db = pd.concat([df.dropna(axis=1, how="all") for df in db], axis=0) + db = _empirical_adjustments(db) + return _formatted_db(db), num_jurisdictions_found + + +def save_db(db, out_dir): + """Write qualitative and quantitative ordinance outputs to disk + + Parameters + ---------- + db : pandas.DataFrame + Ordinance dataset containing the full set of columns listed in + :data:`QUANT_OUT_COLS` and :data:`QUAL_OUT_COLS`, plus the + ``quantitative`` boolean flag that dictates output routing. + out_dir : path-like + Directory where ``qualitative_ordinances.csv`` and + ``quantitative_ordinances.csv`` should be written. The directory + is created by :class:`pathlib.Path` if necessary. + + Notes + ----- + Empty DataFrames short-circuit without creating output files. The + function respects the boolean ``quantitative`` column and assumes it + has already been sanitized by :func:`doc_infos_to_db`. + """ + if db.empty: + return + + out_dir = Path(out_dir) + qual_db = db[~db["quantitative"]][QUAL_OUT_COLS] + quant_db = db[db["quantitative"]][QUANT_OUT_COLS] + qual_db.to_csv(out_dir / "qualitative_ordinances.csv", index=False) + quant_db.to_csv(out_dir / "quantitative_ordinances.csv", index=False) + + +def _db_results(results, doc_info): + """Extract results from doc attrs to DataFrame""" + + results["source"] = doc_info.get("source") + results["year"] = extract_ord_year_from_doc_attrs(doc_info) + + jurisdiction = doc_info["jurisdiction"] + results["FIPS"] = jurisdiction.code + results["county"] = jurisdiction.county + results["state"] = jurisdiction.state + results["subdivision"] = jurisdiction.subdivision_name + results["jurisdiction_type"] = jurisdiction.type + return results + + +def _empirical_adjustments(db): + """Post-processing adjustments based on empirical observations + + Current adjustments include: + + - Limit adder to max of 250 ft. + - Chat GPT likes to report large values here, but in + practice all values manually observed in ordinance documents + are below 250 ft. If large value is detected, assume it's an + error on Chat GPT's part and remove it. + + """ + if "adder" in db.columns: + db.loc[db["adder"] > 250, "adder"] = None # noqa: PLR2004 + return db + + +def _formatted_db(db): + """Format DataFrame for output""" + for col in _PARSED_COLS: + if col not in db.columns: + db[col] = None + + db["quantitative"] = db["quantitative"].astype("boolean").fillna(True) + ord_rows = ordinances_bool_index(db) + return db[ord_rows][_PARSED_COLS].reset_index(drop=True) + + +def _extract_model_info_from_all_models(models): + """Group model info together""" + models_to_tasks = {} + for task, caller_args in models.items(): + models_to_tasks.setdefault(caller_args, []).append(task) + + return [ + { + "name": caller_args.name, + "llm_call_kwargs": caller_args.llm_call_kwargs or None, + "llm_service_rate_limit": caller_args.llm_service_rate_limit, + "text_splitter_chunk_size": caller_args.text_splitter_chunk_size, + "text_splitter_chunk_overlap": ( + caller_args.text_splitter_chunk_overlap + ), + "client_type": caller_args.client_type, + "tasks": tasks, + } + for caller_args, tasks in models_to_tasks.items() + ] + + +def compile_run_summary_message( + total_seconds, total_cost, out_dir, document_count +): + """Create a human-readable summary of a completed run + + Parameters + ---------- + total_seconds : float or int + Duration of the run in seconds. + total_cost : float or int or None + Monetary cost incurred by the run. ``None`` or zero suppresses + the cost line in the summary. + out_dir : path-like + Location of the run output directory. The value is embedded in + the summary text. + document_count : int + Number of documents discovered across all jurisdictions. + + Returns + ------- + str + Summary string formatted for CLI presentation with ``rich`` + markup. + + Notes + ----- + The function does not perform I/O; callers may log or display the + returned string as needed. + """ + runtime = _elapsed_time_as_str(total_seconds) + total_cost = ( + f"\nTotal cost: [#71906e]${total_cost:,.2f}[/#71906e]" + if total_cost + else "" + ) + + return ( + f"✅ Scraping complete!\nOutput Directory: {out_dir}\n" + f"Total runtime: {runtime} {total_cost}\n" + f"Number of documents found: {document_count}" + ) + + +def _elapsed_time_as_str(seconds_elapsed): + """Format elapsed time into human readable string""" + days, seconds = divmod(int(seconds_elapsed), 24 * 3600) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + time_str = f"{hours:d}:{minutes:02d}:{seconds:02d}" + if days: + time_str = f"{days:,d} day{'s' if abs(days) != 1 else ''}, {time_str}" + return time_str diff --git a/compass/utilities/jurisdictions.py b/compass/utilities/jurisdictions.py index 1a3053855..79e56f249 100644 --- a/compass/utilities/jurisdictions.py +++ b/compass/utilities/jurisdictions.py @@ -15,6 +15,7 @@ logger = logging.getLogger(__name__) KNOWN_JURISDICTIONS_REGISTRY = { importlib.resources.files("compass") / "data" / "conus_jurisdictions.csv", + importlib.resources.files("compass") / "data" / "rmp_jurisdictions.csv", } _JUR_COLS = [ "Jurisdiction Type", diff --git a/examples/rmp_demo/README.md b/examples/rmp_demo/README.md new file mode 100644 index 000000000..42bcb1cf6 --- /dev/null +++ b/examples/rmp_demo/README.md @@ -0,0 +1,51 @@ +# COMPASS BLM RMP Geothermal Restriction Extraction + +This directory shows how to run COMPASS to extract geothermal leasing restrictions +from BLM Resource Management Plan (RMP) documents using the +[one-shot schema extraction approach](./one-shot/). + +## Overview + +RMPs are multi-document federal land-use plans issued by BLM field offices. Each +plan may contain closures, No Surface Occupancy (NSO) stipulations, seasonal timing +restrictions, wildlife/water buffers, and other conditions that directly constrain +geothermal leasing, exploration, and drilling. + +This example targets the **Carson City Field Office Consolidated RMP** (Nevada) as +a demonstration, but the same plugin config and schema apply to any BLM RMP. + +## Quick Start + +```bash +compass process \ + -c examples/rmp_demo/one-shot/config.json5 \ + -p examples/rmp_demo/one-shot/plugin_config.yaml +``` + +Or with pixi: + +```bash +pixi run --manifest-path pixi.toml \ + compass process \ + -c examples/rmp_demo/one-shot/config.json5 \ + -p examples/rmp_demo/one-shot/plugin_config.yaml +``` + +## Files + +| File | Description | +|------|-------------| +| `jurisdictions.csv` | List of jurisdictions (County, State) to process | +| `one-shot/config.json5` | Main COMPASS run configuration | +| `one-shot/plugin_config.yaml` | One-shot extraction plugin settings (keywords, prompts, schema ref) | +| `one-shot/rmp_schema.json` | JSON schema defining the geothermal restriction output structure | +| `one-shot/local_docs.json5` | Template for pointing COMPASS at local RMP PDF files | + +## Using Local PDFs + +RMP documents are typically downloaded in advance rather than web-crawled. Edit +`one-shot/local_docs.json5` to point to your local PDF files and set +`known_local_docs` in `config.json5` to that file path. + +See the [COMPASS documentation](https://nrel.github.io/COMPASS/) for more details +on local document configuration. diff --git a/examples/rmp_demo/jurisdictions.csv b/examples/rmp_demo/jurisdictions.csv new file mode 100644 index 000000000..214347d75 --- /dev/null +++ b/examples/rmp_demo/jurisdictions.csv @@ -0,0 +1,2 @@ +County,State +Carson City,Nevada diff --git a/examples/rmp_demo/one-shot/config.json5 b/examples/rmp_demo/one-shot/config.json5 new file mode 100644 index 000000000..3391184c8 --- /dev/null +++ b/examples/rmp_demo/one-shot/config.json5 @@ -0,0 +1,41 @@ +{ + out_dir: "./outputs", + tech: "geothermal_rmps", + jurisdiction_fp: "../jurisdictions.csv", + // Point this at your local_docs.json5 to use pre-downloaded RMP PDFs. + // Remove or set to null to let COMPASS search the web instead. + "known_local_docs": "./local_docs.json5", + + // Disable web/search-engine document discovery when using local PDFs. + "perform_se_search": false, + "perform_website_search": false, + + "log_level": "DEBUG", + + // Path to Tesseract OCR executable (required for scanned PDF text extraction). + "pytesseract_exe_fp": "", + + model: [ + { + name: "", + llm_call_kwargs: {timeout: 300}, + llm_service_rate_limit: 400000, // Tokens per minute - match your Azure quota + text_splitter_chunk_size: 10000, + text_splitter_chunk_overlap: 500, + "client_kwargs": { + // Credentials loaded from env vars: + // AZURE_OPENAI_API_KEY, AZURE_OPENAI_API_VERSION, AZURE_OPENAI_ENDPOINT + }, + }, + ], + + "file_loader_kwargs": { + "verify_ssl": false, + }, + "ppe_kwargs": { + "max_workers": 1, // Prevent concurrent subprocess race on browserforge model files + }, + "td_kwargs": { + "dir": "", + }, +} diff --git a/examples/rmp_demo/one-shot/local_docs.json5 b/examples/rmp_demo/one-shot/local_docs.json5 new file mode 100644 index 000000000..c9266aa6d --- /dev/null +++ b/examples/rmp_demo/one-shot/local_docs.json5 @@ -0,0 +1,11 @@ +{ + // Keys are FIPS codes (string). Each value is a list of local PDF source files + // for that jurisdiction. Set check_if_legal_doc to false for RMP documents. + // + // Carson City, NV FIPS: 32510 + // Example structure — replace source_fp values with your actual PDF paths: + "32510": [ + {"source_fp": "/path/to/rmp/Carson_City_Consolidated_RMP.pdf", "check_if_legal_doc": false}, + {"source_fp": "/path/to/rmp/Amendment_Southern_Washoe.pdf", "check_if_legal_doc": false} + ] +} diff --git a/examples/rmp_demo/one-shot/plugin_config.yaml b/examples/rmp_demo/one-shot/plugin_config.yaml new file mode 100644 index 000000000..a1c5d0db5 --- /dev/null +++ b/examples/rmp_demo/one-shot/plugin_config.yaml @@ -0,0 +1,83 @@ +# One-shot plugin configuration for geothermal restriction extraction from +# BLM Resource Management Plan (RMP) documents. +# +# Usage: +# compass process -c config.json5 -p plugin_config.yaml +# +# Or with pixi: +# pixi run --manifest-path "/pixi.toml" \ +# compass process -c config.json5 -p plugin_config.yaml + +schema: ./rmp_schema.json + +data_type_short_desc: geothermal leasing restriction in BLM Resource Management Plan + +# Keyword-based heuristic for quickly filtering text chunks before LLM calls. +heuristic_keywords: + good_tech_keywords: + - "geothermal" + - "leasing" + - "stipulation" + - "closure" + - "drilling" + - "mineral" + - "no surface occupancy" + - "nso" + - "surface disturbance" + good_tech_acronyms: + - "nso" + - "rmp" + - "blm" + - "tlm" + - "esa" + good_tech_phrases: + - "geothermal leasing" + - "geothermal development" + - "geothermal exploration" + - "mineral leasing" + - "no surface occupancy" + - "surface-disturbing activities" + - "timing limitation" + - "lease stipulation" + - "closed to leasing" + - "open to leasing" + - "geothermal resources" + - "resource management plan" + - "land withdrawal" + not_tech_words: + - "grazing allotment" + - "livestock" + - "recreation permit" + - "campground" + - "trail maintenance" + - "off-highway vehicle" + - "ohv route" + - "land disposal" + - "land sale" + - "residential" + - "wildfire" + - "prescribed burn" + - "hunting permit" + - "fishing" + +# Collection prompts filter individual text chunks extracted from the PDF. +# Chunks that pass are passed to the text extraction step. +# Setting to True auto-generates prompts from the schema. +collection_prompts: True + +# System prompt used during the final structured extraction LLM call. +extraction_system_prompt: |- + You are an expert analyst specializing in BLM Resource Management Plans (RMPs) + and federal mineral leasing regulations. Your task is to extract structured data + about geothermal leasing restrictions from RMP document text. + Follow all instructions in the schema field descriptions carefully. + Only extract restrictions that directly constrain geothermal leasing, exploration, + drilling, or facility construction on BLM lands. Each distinct restriction should + be its own row in the outputs array. Use direct text excerpts and quotes in the + summary field wherever possible. + +cache_llm_generated_content: true + +# Allows a single jurisdiction entry to span multiple PDF documents, +# which is typical for RMPs that are split across many files. +allow_multi_doc_extraction: true diff --git a/examples/rmp_demo/one-shot/rmp_schema.json b/examples/rmp_demo/one-shot/rmp_schema.json new file mode 100644 index 000000000..2459fae01 --- /dev/null +++ b/examples/rmp_demo/one-shot/rmp_schema.json @@ -0,0 +1,386 @@ +{ + "title": "RMP Geothermal Restriction Extraction Schema", + "description": "Single-shot structured extraction schema for geothermal restrictions in BLM Resource Management Plan (RMP) documents. Extract information that directly affects or is likely to affect geothermal leasing, exploration, drilling, or development on BLM lands. A restriction is relevant if it: (1) explicitly mentions geothermal leasing or development, OR (2) applies broadly to energy development, oil/gas/geothermal leasing, or mineral leasing/entry in a way that would encompass geothermal UNLESS the document explicitly excludes geothermal. Use 'geothermal_applicability' to flag whether the restriction directly names geothermal ('direct'), applies broadly and would encompass geothermal ('broad'), or explicitly excludes geothermal ('excluded'). Do NOT extract restrictions that apply only to grazing, recreation, land disposal, residential development, or other activities clearly unrelated to energy or mineral development. If the document contains no relevant information, return an empty array. The output is a flat array where each object represents one extracted data point.", + "version": "1.1.0", + "type": "object", + "required": ["outputs"], + "additionalProperties": false, + "properties": { + "outputs": { + "type": "array", + "items": { + "type": "object", + "required": ["feature", "location", "restriction_type", "geothermal_applicability", "value", "units", "section","summary", "source"], + "additionalProperties": false, + "properties": { + "feature": { + "type": "string", + "description": "The type of restriction or metadata being extracted. Must be one of the enumerated feature IDs. Each closed area, NSO stipulation, seasonal restriction, wildlife restriction, water restriction, and other restriction should be its own row in the output array. Use 'geothermal_closure' for all area closures — whether the document explicitly names geothermal or applies a broader energy/mineral closure that would encompass geothermal. Use 'restriction_type' to describe the scope of what is restricted and 'geothermal_applicability' to flag how directly it applies to geothermal.", + "enum": [ + "document_name", + "document_date", + "rmp_area", + "state", + "geothermal_closure", + "geothermal_closure_with_stipulations", + "no_surface_occupancy", + "controlled_surface_use", + "seasonal_restriction", + "wildlife_restriction", + "water_restriction", + "other_restriction", + "resource_removals", + "resource_additions" + ] + }, + "location": { + "type": ["string", "null"], + "description": "The named geographic area, habitat, water feature, or condition to which this restriction or entry applies. This is the PRIMARY field for identifying WHERE a restriction applies. For restriction features: the place name, area designation, habitat type, or condition triggering the restriction (e.g., 'Jack's Valley', 'Riparian Management Areas', 'Sage Grouse breeding habitat', 'springs and seeps'). For metadata features (document_name, document_date, rmp_area, state): use null. Use null only when no specific location or condition can be identified." + }, + "restriction_type": { + "type": ["string", "null"], + "description": "Describes the scope of what activity is restricted. For geothermal_closure: one of 'geothermal_only' (closure names geothermal specifically), 'all_mineral_leasing' (closed to all mineral leasing — oil, gas, geothermal), 'oil_gas_geothermal' (closed to oil, gas, and geothermal listed as a group), 'all_energy_development' (broad energy development closure), 'oil_gas' (oil and gas only; geothermal not mentioned), 'mineral_entry' (closed to locatable mineral entry under the General Mining Law), 'mineral_leasing' (closed to leasable minerals under the Mineral Leasing Act), or 'all_minerals' (closed to both mineral entry and leasing). For geothermal_closure_with_stipulations: one of 'valid_existing_rights' (WSA/IMP — no new leasing, existing rights honored) or 'conditional' (discretionary closure under specific legal authority, e.g., CMU Act). For no_surface_occupancy: 'no_surface_occupancy'. For controlled_surface_use: 'controlled_surface_use'. For seasonal_restriction: 'seasonal'. For wildlife_restriction: 'wildlife_buffer'. For water_restriction: 'water_buffer'. For other_restriction: a short category label (e.g., 'cultural resources', 'visual resources'). For resource_removals: 'removal'. For resource_additions: 'addition'. For metadata features: use null." + }, + "geothermal_applicability": { + "type": ["string", "null"], + "description": "Indicates how directly this restriction applies to geothermal leasing or development. Use one of three values: 'direct' — the restriction explicitly names geothermal leasing or development; 'broad' — the restriction applies to energy development or mineral leasing broadly and would encompass geothermal unless the document explicitly excludes it; 'excluded' — the restriction explicitly carves geothermal out (e.g., 'closed to oil and gas leasing but open to geothermal'). For metadata features (document_name, document_date, rmp_area, state): use null." + }, + "value": { + "description": "A quantitative or categorical value associated with the restriction, subordinate to location. Type and content depend on the feature: (1) document_name: the full title string; (2) document_date: the publication or approval date string (e.g., 'May 2001'); (3) rmp_area: the BLM field office or planning area name string; (4) state: the state name(s) string; (5) geothermal_closure: acreage if stated (number), otherwise null; (6) geothermal_closure_with_stipulations: acreage if stated (number), otherwise null; (7) no_surface_occupancy: the numeric setback distance if specified (number), otherwise null; (8) controlled_surface_use: null; (9) seasonal_restriction: the restriction period extracted exactly as written — this may be a date range (e.g., 'March 1 - July 30'), a named season (e.g., 'spring', 'breeding season', 'lekking season'), or a descriptive period (e.g., 'during active nesting'); (10) wildlife_restriction: the numeric setback/buffer distance if specified (number), otherwise null; (11) water_restriction: the numeric setback distance if specified (number), otherwise null; (12) other_restriction: a short category label string (e.g., 'cultural resources', 'visual resources'); (13) resource_removals: acreage removed if stated (number), otherwise null; (14) resource_additions: acreage added if stated (number), otherwise null. Use null if no relevant value is found.", + "anyOf": [ + { "type": "number" }, + { "type": "string" }, + { "type": "null" } + ] + }, + "units": { + "type": ["string", "null"], + "description": "Units for the numeric value field only. For numeric setback distances (no_surface_occupancy, wildlife_restriction, water_restriction): use 'feet', 'miles', or 'meters'. For seasonal_restriction: use 'dates'. For geothermal_closure, geothermal_closure_with_stipulations (acreage): use 'acres' if a numeric acreage is given, otherwise null. For resource_removals and resource_additions: 'acres' if a numeric acreage is given, otherwise null. For all other features or when value is null: use null. Do NOT put closure type or restriction category here — those belong in restriction_type." + }, + "section": { + "type": ["string", "null"], + "description": "The section title, number, table, or page reference from the RMP document where this information is found (e.g., 'Table 2-1: Mineral Stipulations', 'Chapter 3: Energy and Minerals', 'Appendix B: Stipulations'). Include the numeric label if provided. Null if no section identifier is available." + }, + "summary": { + "type": ["string", "null"], + "description": "A structured text field with two labeled sections separated by a newline. Format exactly as: 'Summary: [summary text]\\nJustification: [justification text]'. The Summary section (2–4 sentences) covers: (1) the area name and acreage if stated, (2) the exact language used in the document describing what is restricted (quoted where possible), (3) the stated reason or resource being protected, and (4) any conditions, exceptions, or relevant context. Use direct text excerpts and quotes from the document where possible. The Justification section (1–2 sentences) explains why this restriction was included — specifically, how it relates to or could affect geothermal leasing or development (e.g., 'Included because mineral leasing closures under the Mineral Leasing Act encompass geothermal leasing.', 'Included because this NSO stipulation applies to all surface-disturbing activities associated with mineral leasing, which includes geothermal drilling.'). For metadata features, the Justification may be omitted. Null only if no relevant information is found." + }, + "source": { + "type": ["number", "null"], + "description": "Integer indicating the source index from which this information was pulled. If not applicable or unavailable, use null.", + "default": null + } + } + } + } + }, + "$definitions": { + "scope": { + "description": "CRITICAL SCOPE: Extract restrictions that directly affect or are likely to affect geothermal leasing, exploration, drilling, or facility construction on BLM lands. A restriction qualifies if it: (1) explicitly mentions geothermal leasing or development; OR (2) applies broadly to energy development or mineral leasing in a way that would encompass geothermal UNLESS the document explicitly carves geothermal out. High-recall rule: when in doubt about whether a broad energy or mineral closure applies to geothermal, extract it and set geothermal_applicability to 'broad' so downstream users can evaluate applicability. Do NOT extract: restrictions applying only to grazing, recreation, land disposal, residential development, fire management, OHV use, or hunting UNLESS those restrictions also explicitly constrain surface-disturbing activities or mineral/energy development. Closure information may be spread across multiple sections and documents (e.g., main RMP text, amendments) — extract from all sections as its own row in the outputs array. Each distinct restriction should be its own row; do not combine multiple restrictions into one row." + }, + "metadata_features": { + "description": "Document metadata to extract as the first entries in the outputs array.", + "properties": { + "document_name": { + "description": "The full official title of the RMP document. LOCATION: null. VALUE: Full title string. UNITS: null. SUMMARY: Include the full title and any subtitle or amendment information." + }, + "document_date": { + "description": "The publication or approval date of the RMP document. LOCATION: null. VALUE: Date string as written (e.g., 'May 2001', 'September 2008'). UNITS: null. SUMMARY: Include the full date reference and any amendment or revision dates if present." + }, + "rmp_area": { + "description": "The BLM field office or planning area covered by the RMP. LOCATION: null. VALUE: Name of the BLM field office or planning area (e.g., 'Winnemucca District', 'Carson City Field Office'). UNITS: null. SUMMARY: Include any alternative names or geographic extent description." + }, + "state": { + "description": "The U.S. state(s) in which the RMP planning area is located. LOCATION: null. VALUE: State name(s) as written (e.g., 'Nevada', 'California and Nevada'). UNITS: null. SUMMARY: Include full geographic coverage if multi-state." + } + } + }, + "geothermal_closure": { + "description": "Areas where geothermal leasing and/or development is fully closed or prohibited, including both explicit geothermal closures and broader energy or mineral closures that would encompass geothermal. This is the single feature for all area closures regardless of whether the document names geothermal specifically or uses broader language — use geothermal_applicability and restriction_type to capture the distinction. Each closed area should be its own output row — do NOT combine multiple areas into one row. Do NOT include areas that are merely restricted with stipulations such as NSO, timing limitations, or mitigation requirements — those belong in other feature categories. Do NOT include WSAs or areas closed only to NEW leasing while honoring valid existing rights — those belong in geothermal_closure_with_stipulations. Include even if the area is transferred to another jurisdiction (e.g., the USFS). If the same named location is listed under two different closure sections with different scopes (e.g., section 6 'Closed to Oil, Gas and Geothermal Leasing' and section 9 'Closed to Geothermal Leasing Only'), extract it as two separate rows — one for each section. LOCATION: The name of the closed area or land designation (e.g., 'Jack's Valley', 'Grimes Point Archaeological Area', 'Walker Lake'). RESTRICTION_TYPE: The specific scope of the closure — one of 'geothermal_only', 'all_mineral_leasing', 'oil_gas_geothermal', 'all_energy_development', 'oil_gas', 'mineral_entry', 'mineral_leasing', or 'all_minerals'. GEOTHERMAL_APPLICABILITY: 'direct' if geothermal is explicitly named; 'broad' if the closure applies broadly and encompasses geothermal; 'excluded' if geothermal is explicitly carved out. VALUE: Acreage of the closed area if stated (number); otherwise null. UNITS: 'acres' if acreage is given, otherwise null. SUMMARY: 2–4 sentences including area name, acreage, the exact document language describing the closure, the stated reason, and any relevant context such as transfers to another jurisdiction, relationship to other closures, or whether individual acreage was unstated with a group total given." + }, + "geothermal_closure_with_stipulations": { + "description": "Areas closed to NEW geothermal or mineral leasing but where valid existing rights are honored, or areas subject to discretionary/conditional closures under specific legal authority. This is distinct from a full outright closure — leasing is not permitted on new parcels, but existing leases or rights are not extinguished. The primary cases are: (1) Wilderness Study Areas (WSAs) managed under the Interim Management Policy (IMP), which restrict mining and energy development to valid existing rights and prohibit new leasing; (2) lands segregated or withdrawn from mineral entry under specific legal authority (e.g., Classification and Multiple Use Act) where the closure is conditional or discretionary rather than absolute. Each distinct area should be its own row. LOCATION: The name of the area or designation (e.g., 'Clan Alpine Mountains WSA', 'Sun Valley CMU Act lands'). RESTRICTION_TYPE: One of 'valid_existing_rights' (WSA/IMP — no new leasing, existing rights honored) or 'conditional' (discretionary closure under specific legal authority such as the CMU Act or a formal withdrawal process). VALUE: Acreage if stated (number); otherwise null. UNITS: 'acres' if acreage is given, otherwise null. SUMMARY: Include area name, acreage, the legal basis for the restriction (e.g., 'Wilderness Interim Management Policy', 'Classification and Multiple Use Act'), what rights are preserved (e.g., 'valid existing rights honored'), and the stated reason extracted as written." + }, + "no_surface_occupancy": { + "description": "Areas where No Surface Occupancy (NSO) stipulations apply, meaning the lessee cannot drill or place facilities on the surface. A lease can be issued but no surface disturbance is permitted. Each distinct NSO stipulation should be its own output row. Only include NSO stipulations that explicitly apply to geothermal leases, mineral leases, or all surface-disturbing activities (which would encompass geothermal drilling). Do NOT include NSO conditions that only apply to other uses like grazing or recreation. LOCATION: The named area, habitat, or condition triggering the NSO stipulation (e.g., 'Riparian Management Areas', 'cultural resource sites', 'perennial streams'). VALUE: The numeric setback distance if specified (e.g., 500 for '500 feet'); otherwise null. UNITS: Setback distance units ('feet', 'miles', 'meters') if a numeric distance is given; otherwise null. SUMMARY: Include the full area name or condition, setback distance with units, acreage if stated, and all additional details about the NSO stipulation extracted as written. Include stipulation code if provided (e.g., NSO-1)." + }, + "controlled_surface_use": { + "description": "Areas where geothermal leasing and surface use are permitted, but the lessee must comply with specific required mitigation measures or operating constraints (Controlled Surface Use, CSU stipulation). Unlike NSO, the lessee CAN drill and construct facilities, but must follow specific conditions such as seasonal operating windows, reclamation requirements, access restrictions, or site-specific mitigation plans. Each distinct CSU stipulation should be its own output row. Only include CSU stipulations that explicitly apply to geothermal leases, mineral leases, or surface-disturbing energy development activities. LOCATION: The named area or condition triggering the CSU stipulation (e.g., 'Sage Grouse Habitat — East Walker River Area'). VALUE: null. UNITS: null. SUMMARY: Include the area name, the specific mitigation measures or operating constraints required (extracted as written), acreage if stated, the resource or value being protected, and any conditions or exceptions. Include stipulation code if provided (e.g., CSU-1)." + }, + "seasonal_restriction": { + "description": "Time-based restrictions that limit WHEN geothermal exploration, drilling, or construction activities can occur. These are seasonal timing limitations on surface-disturbing activities that would apply to geothermal operations. Each distinct seasonal restriction should be its own output row. Only include restrictions on 'surface-disturbing activities', 'drilling', 'construction', 'mineral leasing activities', or similarly broad categories encompassing geothermal work. Do NOT include seasonal restrictions that only apply to grazing, hunting, recreation, or OHV use. LOCATION: The named area, habitat, species range, or geographic location where the seasonal restriction applies. This may be a species habitat name (e.g., 'Sage Grouse breeding habitat', 'Prairie Falcon Habitat'), a geographic area name (e.g., 'North of Cold Springs', 'East Walker River Area', 'Pine Nut Mountains'), or any other named place. Do NOT require a species name — geographic area names are equally valid. VALUE: The restriction period extracted exactly as written — may be a specific date range (e.g., 'March 1 - July 30'), a named season (e.g., 'spring', 'breeding season', 'lekking season'), or a descriptive period (e.g., 'during active nesting'). Do NOT skip a seasonal restriction just because it uses a season name rather than explicit dates. UNITS: 'dates'. SUMMARY: Include the area name or habitat, the species or resource being protected (if specified), the full restriction period as written, the type of activity restricted (extracted as written, e.g., 'all surface-disturbing activities', 'drilling operations'), and the acreage affected if stated." + }, + "wildlife_restriction": { + "description": "Year-round wildlife protections that would constrain WHERE geothermal facilities, wells, or access roads could be placed (Seasonal wildlife restrictions do not fall under this category. They fall under seasonal_restrictions). Each distinct wildlife restriction should be its own output row. Only include restrictions that apply to surface-disturbing activities, mineral development, or energy development. Examples: habitat buffer zones prohibiting surface disturbance, endangered species consultation requirements for energy projects, wildlife corridors where development is restricted. Do NOT include restrictions that only apply to grazing management, livestock, hunting, or OHV use — unless they also explicitly restrict surface-disturbing activities or mineral/energy development. LOCATION: The species name, habitat type, or named area the restriction protects (e.g., 'active Prairie Falcon nest sites', 'Greater Sage-Grouse habitat'). VALUE: The numeric setback or buffer distance if specified (number); otherwise null. UNITS: Setback distance units ('feet', 'miles', 'meters') if a numeric distance is given; otherwise null. SUMMARY: Include the species or habitat being protected, the type of restriction (e.g., 'habitat avoidance', 'buffer zone', 'no surface disturbance', 'mitigation required', 'no surface occupancy'), the specific area or land unit where the restriction applies if named, the setback distance with units, and all additional details extracted as written." + }, + "water_restriction": { + "description": "Water resource protections that would constrain geothermal drilling, facility siting, or surface disturbance near water features (This is a distinct category, do not replicate NSO's). Each distinct water restriction should be its own output row. Only include restrictions that apply to surface-disturbing activities, mineral leasing, or energy development near water. Examples: NSO setbacks from streams for mineral leases, riparian buffers prohibiting surface disturbance, groundwater monitoring requirements for drilling. Do NOT include water restrictions that only apply to grazing, irrigation, or general land management. LOCATION: The type of water feature or named water body the restriction applies to (e.g., 'springs, seeps, and wetlands', 'perennial and intermittent streams', 'Riparian Management Areas'). VALUE: The numeric setback distance if specified (number); otherwise null. UNITS: Setback distance units ('feet', 'miles', 'meters') if a numeric distance is given; otherwise null. SUMMARY: Include the type of water feature or resource, the type of restriction (e.g., 'no surface occupancy', 'setback', 'avoidance', 'monitoring required'), the setback distance with units, and any additional context or conditions extracted as written." + }, + "other_restriction": { + "description": "Other restrictions that DIRECTLY constrain geothermal leasing, exploration, drilling, or facility construction but do not fit the categories above. Each distinct restriction should be its own output row. Examples: cultural resource protections requiring archaeological surveys before ground disturbance, visual resource management classes limiting industrial development, air quality permits required for geothermal operations, tribal consultation requirements for energy projects, land withdrawals from mineral entry. Do NOT include: general land management policies, recreational use rules, grazing regulations, land disposal plans, residential development constraints, fire management, or any restriction that does not directly affect geothermal or mineral/energy development. Return no rows with this feature if no such restrictions exist. LOCATION: The area or condition the restriction applies to; null if it applies planning-area wide. VALUE: A short category label string (e.g., 'cultural resources', 'visual resources', 'air quality', 'tribal consultation', 'land withdrawal'). UNITS: null. SUMMARY: Include the area or condition the restriction applies to and the full restriction details extracted as written." + }, + "resource_removals": { + "description": "Areas or acreage removed from availability for geothermal leasing or mineral/energy development in the RMP. Include: acreage removed from geothermal or mineral leasing availability, wilderness or WSA designations that withdraw land from mineral entry, land transfers or disposals that remove federal mineral estate, KGRA reductions, and any other action that explicitly reduces the area open to geothermal leasing. Each distinct removal action should be its own row. Do NOT include closures with stipulations (NSO, timing limits) — those belong in other categories. LOCATION: The name of the area or designation being removed (e.g., 'Warm Springs area', 'Clan Alpine WSA'). VALUE: The numeric acreage removed if specified (number); otherwise null. UNITS: 'acres' if a numeric acreage is given; otherwise null. SUMMARY: Include the area name, acreage removed, reason stated, the specific action taken (e.g., 'withdrawn from mineral entry', 'removed from geothermal leasing availability', 'designated as wilderness'), and the document or amendment that enacted the change. Use direct quotes where possible." + }, + "resource_additions": { + "description": "Areas or acreage added to or opened for geothermal leasing or mineral/energy development in the RMP. Include: acreage newly opened to geothermal or mineral leasing, KGRA designations or expansions, land re-entries into the federal leasing pool, and any other action that explicitly increases the area available for geothermal leasing. Each distinct addition should be its own row. LOCATION: The name of the area or designation being added (e.g., 'Steamboat Known Geothermal Resource Area (KGRA)'). VALUE: The numeric acreage added if specified (number); otherwise null. UNITS: 'acres' if a numeric acreage is given; otherwise null. SUMMARY: Include the area name, acreage added, reason or justification stated, the specific action taken (e.g., 'opened to geothermal leasing', 'designated as KGRA'), and the document or amendment that enacted the change. Use direct quotes where possible." + } + }, + "$examples": [ + { + "feature": "document_name", + "location": null, + "value": "Winnemucca District Record of Decision and Resource Management Plan", + "units": null, + "section": "Cover Page", + "summary": "Full title: 'Winnemucca District Record of Decision and Resource Management Plan'. Approved by the BLM Winnemucca District." + }, + { + "feature": "geothermal_closure", + "location": "Walker Lake", + "restriction_type": "oil_gas_geothermal", + "geothermal_applicability": "broad", + "value": null, + "units": null, + "section": "Minerals and Energy: Areas Closed to Oil, Gas and Geothermal Leasing", + "summary": "Walker Lake is closed to oil, gas, and geothermal leasing as part of 'Key Scenic, Wildlife, Recreation, and Historic Areas' totaling 45,392 acres. Document language: 'Areas Closed to Oil, Gas and Geothermal Leasing.' Individual acreage not specified; group total is 45,392 acres.", + "source": 0 + }, + { + "feature": "geothermal_closure", + "location": "Grimes Point Archaeological Area", + "restriction_type": "mineral_entry", + "geothermal_applicability": "broad", + "value": 400, + "units": "acres", + "section": "Minerals and Energy: Areas Closed to Mineral Entry (22,672 Acres)", + "summary": "Grimes Point Archaeological Area (400 acres) is closed to mineral entry under the General Mining Law. Stated reason: cultural resource protection. Note: a separate geothermal-specific closure also exists for this area (640 acres) under section 9.", + "source": 0 + }, + { + "feature": "geothermal_closure", + "location": "Jack's Valley", + "restriction_type": "all_mineral_leasing", + "value": 1200, + "units": "acres", + "section": "Table 2-1: Mineral Stipulations", + "summary": "Jack's Valley (1,200 acres) is closed to all mineral leasing including geothermal. Reason stated: 'Key Scenic, Wildlife, Recreation, and Historic Areas'. Closure applies to oil, gas, and geothermal leasing." + }, + { + "feature": "geothermal_closure", + "location": "Grimes Point Archaeological Area", + "restriction_type": "geothermal_only", + "value": null, + "units": null, + "section": "Chapter 3: Energy and Minerals, Special Designations", + "summary": "Grimes Point Archaeological Area is closed specifically to geothermal leasing. Stated reason: 'cultural resource protection'. Acreage not specified in document." + }, + { + "feature": "no_surface_occupancy", + "location": "perennial and intermittent streams, rivers, lakes, and reservoirs", + "restriction_type": "no_surface_occupancy", + "value": 500, + "units": "feet", + "section": "Appendix B: Lease Stipulations, Stipulation NSO-1", + "summary": "No Surface Occupancy stipulation applies within 500 feet of any perennial or intermittent stream, river, lake, or reservoir. Applies to all geothermal and mineral lease activities. No acreage specified. Details: 'No surface-disturbing activities associated with mineral leasing operations will be permitted within 500 feet of any water body.'" + }, + { + "feature": "no_surface_occupancy", + "location": "Riparian Management Areas", + "restriction_type": "no_surface_occupancy", + "value": null, + "units": null, + "section": "Appendix B: Lease Stipulations, Stipulation NSO-4", + "summary": "No Surface Occupancy applies within all designated Riparian Management Areas. No numeric setback distance specified — restriction applies to the full extent of the riparian management zone. Approximately 3,400 acres affected. Applies to all surface-disturbing activities associated with mineral leasing." + }, + { + "feature": "seasonal_restriction", + "location": "Sage Grouse breeding habitat", + "restriction_type": "seasonal", + "geothermal_applicability": "direct", + "value": "March 1 - July 30", + "units": "dates", + "section": "Appendix B: Lease Stipulations, Stipulation TL-3", + "summary": "Seasonal timing limitation applies in Sage Grouse breeding habitat. Species protected: Sage Grouse. Restriction period: March 1 through July 30. Activity restricted: 'all surface-disturbing activities'. Approximately 45,000 acres affected. Applies to geothermal and all mineral leasing operations within designated Sage Grouse habitat areas." + }, + { + "feature": "seasonal_restriction", + "location": "North of Cold Springs", + "restriction_type": "seasonal", + "geothermal_applicability": "direct", + "value": "spring", + "units": "dates", + "section": "Areas Where Some Restrictions Apply to Geothermal Leasing: Seasonal Restrictions, Spring Restrictions", + "summary": "Spring seasonal restriction applies north of Cold Springs (referenced in Fort Churchill/Clan Alpine Geothermal EAR 1975). Restriction period: spring season. Activity restricted: surface-disturbing activities associated with geothermal leasing operations. No acreage specified for this subarea." + }, + { + "feature": "wildlife_restriction", + "location": "active Prairie Falcon nest sites", + "restriction_type": "wildlife_buffer", + "value": 0.25, + "units": "miles", + "section": "Chapter 3: Wildlife, Special Status Species", + "summary": "No surface disturbance allowed within 0.25 miles of active Prairie Falcon nest sites. Species: Prairie Falcon (special status). Restriction type: buffer zone / no surface disturbance. Applies to all energy development and surface-disturbing activities associated with mineral leasing. Details extracted as written: 'Avoid all surface-disturbing activities within one-quarter mile of active Prairie Falcon nesting sites.'" + }, + { + "feature": "water_restriction", + "location": "springs, seeps, and wetlands", + "restriction_type": "water_buffer", + "value": 300, + "units": "feet", + "section": "Appendix B: Lease Stipulations, Stipulation NSO-2", + "summary": "No Surface Occupancy setback of 300 feet from all springs, seeps, and wetlands. Restriction type: no surface occupancy. Applies to all geothermal and mineral leasing surface-disturbing activities. Details: 'No surface occupancy or surface-disturbing activities will be permitted within 300 feet of springs, seeps, or wetland areas.'" + }, + { + "feature": "other_restriction", + "location": null, + "restriction_type": "cultural resources", + "value": "cultural resources", + "units": null, + "section": "Chapter 3: Cultural Resources", + "summary": "Applies planning-area wide to all ground-disturbing activities associated with geothermal or mineral leasing. Full restriction: 'Prior to any ground-disturbing activities, a Class III cultural resource inventory must be completed and approved by the authorized BLM officer. Any identified cultural resources must be avoided or a mitigation plan approved before surface disturbance may proceed.'" + }, + { + "feature": "resource_removals", + "location": "Warm Springs area", + "restriction_type": "removal", + "value": 2956, + "units": "acres", + "section": "Plan Amendment Decision: Southern Washoe County Urban Interface", + "summary": "'Removed all 2,956 acres available for geothermal leasing in the Warm Springs area due to the lack of a sufficient resource for development and no existing leases.' Action: removed from geothermal leasing availability. Reason: insufficient geothermal resource and no existing leases. Enacted by Southern Washoe County Urban Interface Plan Amendment (2001)." + }, + { + "feature": "geothermal_closure_with_stipulations", + "location": "Clan Alpine Mountains WSA", + "restriction_type": "valid_existing_rights", + "value": 196128, + "units": "acres", + "section": "Chapter 2: Wilderness Study Areas, Leasable Minerals SOP", + "summary": "'Clan Alpine WSA 196,128 acres.' Managed under the Wilderness Interim Management Policy (IMP): 'restrict mining and energy development activities to those that are allowed under valid existing rights and do not impair wilderness quality.' No new geothermal or mineral leasing permitted. Valid existing rights honored. If designated as wilderness by Congress, will be fully closed to mineral entry." + }, + { + "feature": "geothermal_closure_with_stipulations", + "location": "Sun Valley, Washoe Valley, Steamboat and Peavine Mountain CMU Act lands", + "restriction_type": "conditional", + "value": 8000, + "units": "acres", + "section": "Chapter 2: Mineral Entry and Energy Development", + "summary": "'Approximately 8,000 acres in Sun Valley, Washoe Valley, Steamboat and Peavine Mountain' classified under the Classification and Multiple Use Act. These are discretionary closures — lands segregated from mineral entry under specific legal authority. New mineral leasing and energy development not permitted under current classification." + }, + { + "feature": "controlled_surface_use", + "location": "Sage Grouse Habitat — East Walker River Area", + "restriction_type": "controlled_surface_use", + "value": null, + "units": null, + "section": "Appendix B: Lease Stipulations, Stipulation CSU-2", + "summary": "Controlled Surface Use stipulation applies within designated Sage Grouse habitat in the East Walker River Area. Leasing and surface operations are permitted but subject to required mitigation: operator must prepare and implement a site-specific mitigation plan minimizing disturbance to sage grouse habitat, including requirements for revegetation with native species, limitations on infrastructure footprint, and reporting to BLM. Stipulation code: CSU-2." + }, + { + "feature": "resource_additions", + "location": "Steamboat Known Geothermal Resource Area (KGRA)", + "restriction_type": "addition", + "value": 1933, + "units": "acres", + "section": "Plan Amendment Decision: Southern Washoe County Urban Interface, Map 3", + "summary": "'Geothermal leasing on 1,933 acres in and adjacent to the Steamboat Known Geothermal Resource Area (KGRA)' retained as open to leasing within an otherwise closed planning area. Action: 1,933 acres explicitly kept open for geothermal leasing. Reason: active KGRA with existing geothermal development potential. Enacted by Southern Washoe County Urban Interface Plan Amendment (2001)." + } + ], + "$instructions": { + "general": [ + "Extract restrictions that directly affect OR are broadly likely to affect geothermal leasing, exploration, drilling, or facility construction on BLM lands. This includes: (1) explicit geothermal closures or stipulations; (2) closures to oil, gas, and geothermal leasing as a group; (3) closures to all energy development; (4) closures to mineral leasing (Mineral Leasing Act — which covers geothermal); (5) closures to mineral entry (General Mining Law — which restricts surface access and land availability). Set geothermal_applicability to 'broad' for categories 2–5. Skip restrictions unrelated to surface disturbance, mineral development, or energy development (e.g., grazing rules, recreation fee programs, fire management).", + "Each closed area, WSA, NSO stipulation, CSU stipulation, seasonal restriction, wildlife restriction, water restriction, and other restriction should be its own row in the outputs array. Do not combine multiple distinct restrictions into a single row.", + "Always begin the outputs array with the four metadata features (document_name, document_date, rmp_area, state) before listing any restrictions.", + "The 'location' field is the primary carrier for WHERE a restriction applies — always populate it for restriction features. Use 'value' for quantitative data (acreage, setback distance, date range) and 'units' for what that number means.", + "In the summary field, always use the two-section format: 'Summary: [text]\\nJustification: [text]'. Use direct text excerpts and quotes in the Summary section. The Justification section must explain how this restriction relates to or could affect geothermal leasing.", + "Only extract enacted requirements. Do not extract definitions, proposed actions, or alternatives that were not selected in the Record of Decision.", + "If a category has no qualifying restrictions, omit rows for that feature entirely rather than returning null values.", + "If a document section references a stipulation by code (e.g., 'NSO-1', 'TL-3'), include that code in the section field." + ], + "energy_closure_guidance": [ + "Use 'geothermal_closure' when the document closes an area to oil, gas, and geothermal leasing as a group, or to 'energy development' broadly. Set restriction_type to 'oil_gas_geothermal' when the document explicitly names all three; use 'all_energy_development' for broader language; use 'oil_gas' when only oil and gas are named and geothermal is not mentioned. Set geothermal_applicability to 'broad' for all of these.", + "If the same area appears under both a broad energy closure section (e.g., 'Areas Closed to Oil, Gas and Geothermal Leasing') AND a geothermal-specific section, extract two separate rows — both as 'geothermal_closure' with the appropriate restriction_type and geothermal_applicability for each.", + "When a section lists multiple named places under a group heading without individual acreages, extract EACH named place as its own row with value null. Include the group total acreage in the summary." + ], + "mineral_closure_guidance": [ + "Use 'geothermal_closure' when the document closes an area to 'mineral entry' (General Mining Law — locatable minerals) or to 'mineral leasing' (Mineral Leasing Act — includes oil, gas, geothermal, coal). Set geothermal_applicability to 'broad' for these.", + "'Mineral entry' and 'mineral leasing' are legally distinct: mineral entry covers locatable minerals (gold, silver, copper) and does not directly govern geothermal; mineral leasing covers leasable minerals including geothermal. Set restriction_type accordingly: 'mineral_entry', 'mineral_leasing', or 'all_minerals' if both are covered.", + "If an area is closed to mineral entry AND also separately closed to oil/gas/geothermal leasing, extract each as its own row — both as 'geothermal_closure' with different restriction_type values.", + "When a section lists multiple named areas under a total acreage header, extract each named area as its own row." + ], + "geothermal_closure_guidance": [ + "Distinguish between areas closed to geothermal leasing only vs. all mineral leasing. Use 'geothermal_only' or 'all_mineral_leasing' as the restriction_type value.", + "Do not include areas with NSO, timing limitation, CSU, or mitigation stipulations as closures — those are restrictions, not closures.", + "Do not include WSAs or areas where existing rights are honored — those belong in geothermal_closure_with_stipulations.", + "Wilderness Areas formally designated by Congress and National Monuments are fully closed to mineral leasing — use geothermal_closure for these.", + "Put the area name in location. Put acreage in value (number) if stated, otherwise null. Use the phrase 'Acreage not specified' in summary if acreage is not given.", + "IMPORTANT: If an area appears in a list headed 'Closed to Oil, Gas and Geothermal Leasing' or similar, but has a note such as 'Transferred to USFS' or 'Transferred to NPS', it MUST still be extracted as a geothermal_closure. The jurisdictional transfer does not remove the closure from the document — extract the closure exactly as listed and note the transfer in the summary. Do NOT skip an area just because it was transferred to another agency.", + "When a closure section lists multiple named places under a group heading (e.g., 'Key Scenic, Wildlife, Recreation, and Historic Areas'), extract EACH named place as its own separate row — do NOT combine them into a single row. Set value to null if no individual acreage is stated for that place; include the section's group total acreage in the summary if given.", + "A location may appear under multiple distinct closure sections in the same RMP (e.g., once under 'Areas Closed to Mineral Entry' and again under 'Areas Closed to Geothermal Leasing Only'). Extract EACH appearance as its own separate row with the appropriate feature, units, acreage, and section. Do NOT deduplicate based on location name.", + "A location may also appear in both a closure section AND a restriction section (e.g., NSO or timing limitation). If the document clearly states the area is CLOSED (no leasing permitted) in one section and separately imposes an NSO or other stipulation in another, extract a geothermal_closure row AND the applicable restriction row — both are valid and should be included." + ], + "geothermal_closure_with_stipulations_guidance": [ + "Use this feature for areas closed to NEW leasing only, where valid existing rights are still honored — the primary case is Wilderness Study Areas (WSAs) managed under the Interim Management Policy (IMP).", + "Also use for lands segregated or withdrawn under discretionary legal authority (e.g., Classification and Multiple Use Act, formal withdrawal processes) where the closure is conditional rather than absolute — set restriction_type to 'conditional'.", + "Each distinct area or designation should be its own row — do not combine multiple WSAs into one row.", + "Put the area name in location. Put acreage in value (number) if stated, otherwise null. Include the legal basis and acreage in the summary.", + "Do NOT use this feature for outright closures with no exceptions — those belong in geothermal_closure.", + "Do NOT use this feature for areas with surface stipulations (NSO, CSU, timing) where leasing itself is still open — those belong in their respective stipulation features." + ], + "no_surface_occupancy_guidance": [ + "NSO means the lessee cannot occupy or disturb the surface even if a lease is issued. This is distinct from a full closure (no lease issued), from a CSU (surface use allowed with conditions), and from a timing limitation (only restricted during certain periods).", + "Put the area name or condition in location. If a numeric setback distance is given, use it as value with appropriate distance units. If no numeric distance applies, value is null.", + "Include stipulation codes (e.g., NSO-1) in the section field when available." + ], + "controlled_surface_use_guidance": [ + "CSU means the lessee CAN drill and construct facilities, but must comply with specific required operating constraints or mitigation measures. This is less restrictive than NSO (which prohibits all surface use) but more restrictive than a standard lease.", + "Only extract CSU stipulations that explicitly apply to geothermal leases, mineral leases, or surface-disturbing energy development activities.", + "Put the area name or condition in location. Value is null for CSU.", + "Include the specific mitigation measures or operating constraints in the summary — these are the core of what makes a CSU distinct.", + "Include stipulation codes (e.g., CSU-1) in the section field when available.", + "Do not confuse CSU with NSO — if the document says 'no surface occupancy' or 'no surface disturbance permitted', use no_surface_occupancy instead." + ], + "seasonal_restriction_guidance": [ + "Put the area or habitat name in location — this may be a species habitat name (e.g., 'Sage Grouse Strutting Grounds', 'Prairie Falcon Habitat') OR a geographic area name (e.g., 'North of Cold Springs', 'East Walker River Area', 'Pine Nut Mountains'). Do NOT require a species or habitat name — geographic names are equally valid locations for seasonal restrictions.", + "When a seasonal restriction section lists multiple named areas (e.g., 'A. Sage Grouse Strutting Grounds' and 'B. North of Cold Springs'), extract EACH named area as its own separate row — do NOT combine them or skip geographic area names.", + "Extract the restriction period exactly as written — do NOT skip a restriction because it uses a season name ('spring', 'breeding season', 'lekking season') rather than specific calendar dates. Record the season name as-is in the value field.", + "Only include timing restrictions that apply to activities that would encompass geothermal drilling or construction (e.g., 'surface-disturbing activities', 'drilling', 'construction', 'mineral leasing operations').", + "Do not extract timing restrictions limited to grazing, livestock, hunting, OHV use, or recreation unless they are explicitly tied to geothermal or mineral leasing activities." + ], + "wildlife_restriction_guidance": [ + "Put the species name, habitat type, or feature being protected in location (e.g., 'active Prairie Falcon nest sites', 'sage-grouse strutting grounds'). Put the numeric setback distance in value if stated.", + "Year-round buffer zones or avoidance areas around wildlife habitat that prohibit surface disturbance qualify — these constrain where geothermal facilities can be placed.", + "Seasonal wildlife restrictions (time-based) should use the 'seasonal_restriction' feature instead.", + "Endangered species consultation requirements that apply broadly to energy development qualify as other_restriction, not wildlife_restriction, unless a specific buffer distance or avoidance area is defined." + ], + "water_restriction_guidance": [ + "Put the water feature type or named water body in location (e.g., 'springs, seeps, and wetlands', 'perennial and intermittent streams'). Put the numeric setback distance in value if stated.", + "NSO setbacks from streams or water bodies for mineral leases qualify — use 'no_surface_occupancy' if the stipulation is explicitly an NSO, or 'water_restriction' if it is a broader avoidance or monitoring requirement.", + "If both NSO and water feature overlap (e.g., NSO within 500 feet of streams), classify as 'no_surface_occupancy' with water feature context in the summary.", + "Groundwater monitoring requirements tied to drilling operations qualify." + ], + "other_restriction_guidance": [ + "Put the area or condition the restriction applies to in location; use null if it applies planning-area wide.", + "Cultural resource survey requirements before ground disturbance qualify.", + "Visual Resource Management (VRM) Class designations that explicitly prohibit industrial development qualify.", + "Land withdrawals from mineral entry qualify — include withdrawal type and acreage in summary.", + "Tribal consultation requirements specifically tied to energy or mineral development qualify.", + "Do not extract general fire management, grazing, or recreation policies." + ], + "resource_removals_guidance": [ + "Put the area name in location. Put acreage in value (number) if stated, otherwise null.", + "Extract any action that explicitly reduces the land area available for geothermal or mineral leasing — this includes acreage reductions, KGRA contractions, wilderness or WSA designations withdrawing land from mineral entry, land transfers conveying the mineral estate, and plan amendments that close previously open areas.", + "Wilderness Study Areas (WSAs) managed under the Interim Management Policy (IMP) are closed to new mineral leasing — extract each WSA as a resource_removal if the document states or implies it was previously open or available.", + "Each distinct removal action should be its own row — do not combine multiple areas into one row.", + "Include the reason for the removal in the summary (e.g., 'lack of sufficient resource', 'wilderness designation', 'land sale', 'urban interface protection').", + "Do not duplicate entries already captured as geothermal_closure — use resource_removals for actions that explicitly change the inventory of leasable acreage rather than applying a leasing restriction to land that remains in the federal estate." + ], + "resource_additions_guidance": [ + "Put the area name in location. Put acreage in value (number) if stated, otherwise null.", + "Extract any action that explicitly increases the land area available for geothermal or mineral leasing — KGRA designations, plan amendments opening previously closed areas, re-entries of land into the federal leasing pool, and decisions to make additional acreage available for leasing.", + "Each distinct addition should be its own row.", + "Include the justification or reason for the addition in the summary.", + "Do not extract general statements that land 'may be' or 'could be' leased in the future — only extract enacted decisions that open specific areas." + ] + } +} From 67c449cc03d7a5c200ebf0835c406bca47105ac7 Mon Sep 17 00:00:00 2001 From: Colby Smith Date: Tue, 26 May 2026 14:40:50 -0700 Subject: [PATCH 2/2] Add finalize hook, PDF OCR fixes, and env scripts Allow schema-based one-shot plugins to write extracted data via a configurable finalize module (default or "rmp"). Fix PDF reading/OCR: strip unexpected kwargs before calling pdftotext, forward image/convert kwargs into OCR path, and patch pytesseract cleanup on Windows to suppress OSErrors when removing temp files. Adjust finalize_rmp import to match renamed helper, and add local_activate.{sh,bat} plus register the Windows activation script in pixi.toml to support loading a local .env. --- compass/plugin/one_shot/base.py | 14 ++++++++++++++ compass/services/cpu.py | 24 +++++++++++++++++++++++- compass/utilities/finalize_rmp.py | 2 +- local_activate.bat | 6 ++++++ local_activate.sh | 7 +++++++ pixi.toml | 3 +++ 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 local_activate.bat create mode 100644 local_activate.sh diff --git a/compass/plugin/one_shot/base.py b/compass/plugin/one_shot/base.py index d7ca83ee9..02bfc55c7 100644 --- a/compass/plugin/one_shot/base.py +++ b/compass/plugin/one_shot/base.py @@ -17,6 +17,8 @@ OrdinanceExtractionPlugin, KeywordBasedHeuristic, ) +import compass.utilities.finalize as _finalize_default +import compass.utilities.finalize_rmp as _finalize_rmp from compass.plugin.one_shot.generators import ( generate_query_templates, generate_website_keywords, @@ -223,6 +225,18 @@ class SchemaBasedExtractionPlugin(OrdinanceExtractionPlugin): WEBSITE_KEYWORDS = {} # set by user or LLM-generated """dict: Keyword weight mapping for link crawl prioritization""" + @classmethod + def save_structured_data(cls, doc_infos, out_dir): + """Write extracted data using finalize module from config""" + fin = ( + _finalize_rmp + if config.get("finalize") == "rmp" + else _finalize_default + ) + db, num_docs_found = fin.doc_infos_to_db(doc_infos) + fin.save_db(db, out_dir) + return num_docs_found + async def get_heuristic(self): """Get a `BaseHeuristic` instance with a `check()` method diff --git a/compass/services/cpu.py b/compass/services/cpu.py index 63cccc51e..c63884855 100644 --- a/compass/services/cpu.py +++ b/compass/services/cpu.py @@ -273,6 +273,8 @@ def _read_pdf_ocr(pdf_bytes, tesseract_cmd, **kwargs): def _read_pdf_file(pdf_fp, **kwargs): """Utility func so that pdftotext.PDF doesn't have to be pickled""" + kwargs.pop("image_to_string_kwargs", None) + kwargs.pop("convert_from_bytes_kwargs", None) pdf_bytes = Path(pdf_fp).read_bytes() pages = read_pdf(pdf_bytes, verbose=False) return PDFDocument(pages, **kwargs), pdf_bytes @@ -283,8 +285,16 @@ def _read_pdf_file_ocr(pdf_fp, tesseract_cmd, **kwargs): if tesseract_cmd: _configure_pytesseract(tesseract_cmd) + image_to_string_kwargs = kwargs.pop("image_to_string_kwargs", None) + convert_from_bytes_kwargs = kwargs.pop("convert_from_bytes_kwargs", None) + pdf_bytes = Path(pdf_fp).read_bytes() - pages = read_pdf_ocr(pdf_bytes, verbose=False) + pages = read_pdf_ocr( + pdf_bytes, + verbose=True, + image_to_string_kwargs=image_to_string_kwargs, + convert_from_bytes_kwargs=convert_from_bytes_kwargs, + ) doc = PDFDocument(_try_decode_ocr_pages(pages), **kwargs) doc.attrs["from_ocr"] = True return doc, pdf_bytes @@ -366,9 +376,21 @@ def _read_file_docling(fp, **kwargs): def _configure_pytesseract(tesseract_cmd): """Set the tesseract_cmd""" import pytesseract # noqa: PLC0415 + from glob import iglob # noqa: PLC0415 + from os import remove # noqa: PLC0415 pytesseract.pytesseract.tesseract_cmd = tesseract_cmd + # On Windows, Tesseract may still hold the temp PPM file open when + # pytesseract's cleanup runs, causing WinError 32. Patch cleanup to + # suppress all OSErrors so the OCR result is not lost. + def _cleanup_win(temp_name): + for filename in iglob(f'{temp_name}*' if temp_name else temp_name): + with contextlib.suppress(OSError): + remove(filename) + + pytesseract.pytesseract.cleanup = _cleanup_win + def _try_decode_ocr_pages(pages): """Try to decode pages into strings""" diff --git a/compass/utilities/finalize_rmp.py b/compass/utilities/finalize_rmp.py index 1d9aae81b..e2791b5eb 100644 --- a/compass/utilities/finalize_rmp.py +++ b/compass/utilities/finalize_rmp.py @@ -10,7 +10,7 @@ from compass import __version__ as compass_version from compass.utilities.parsing import ( - extract_ord_year_from_doc_attrs, + extract_year_from_doc_attrs as extract_ord_year_from_doc_attrs, num_ordinances_dataframe, ordinances_bool_index, ) diff --git a/local_activate.bat b/local_activate.bat new file mode 100644 index 000000000..fc65d3e82 --- /dev/null +++ b/local_activate.bat @@ -0,0 +1,6 @@ +@echo off +for /f "usebackq tokens=1,2 delims==" %%a in ("%~dp0.env") do ( + if not "%%a"=="" if not "%%a:~0,1%"=="#" ( + set "%%a=%%b" + ) +) diff --git a/local_activate.sh b/local_activate.sh new file mode 100644 index 000000000..5e691a5a6 --- /dev/null +++ b/local_activate.sh @@ -0,0 +1,7 @@ +# Source local .env file if it exists (gitignored, never committed) +ENV_FILE="$(dirname "$0")/.env" +if [ -f "$ENV_FILE" ]; then + set -a + source "$ENV_FILE" + set +a +fi diff --git a/pixi.toml b/pixi.toml index b7a303ec2..f6fd48fe2 100644 --- a/pixi.toml +++ b/pixi.toml @@ -12,6 +12,9 @@ ptest = { features = ["python-default", "elm", "python-test"], solve-group = "py pdoc = { features = ["python-default", "elm", "python-doc"], solve-group = "python" } pbuild = { features = ["python-default", "elm", "python-build"], solve-group = "python" } +[activation] +scripts = ["local_activate.bat"] + [tasks] openai-solar-demo = { cmd = "export OPENAI_API_KEY={{ api_key }}; compass process -c config.json5", args = ["api_key"], cwd = "examples/openai_solar_demo"}