| 
 | 1 | +"""  | 
 | 2 | +Build MESSAGEix-Nexus scenario with subannual (monthly) timeslices.  | 
 | 3 | +
  | 
 | 4 | +This script implements a full pipeline for creating a water-energy nexus model  | 
 | 5 | +with monthly temporal resolution:  | 
 | 6 | +1. Load base annual scenario  | 
 | 7 | +2. Add monthly timeslices  | 
 | 8 | +3. Validate timesliced scenario  | 
 | 9 | +4. Build nexus module on timesliced scenario  | 
 | 10 | +5. Solve final subannual nexus model  | 
 | 11 | +"""  | 
 | 12 | + | 
 | 13 | +import logging  | 
 | 14 | + | 
 | 15 | +from message_ix_models import Context  | 
 | 16 | +from message_ix_models.model.water.build import main as build_nexus  | 
 | 17 | +from message_ix_models.model.water.cli import water_ini  | 
 | 18 | +from message_ix_models.project.alps.timeslice import add_timeslices  | 
 | 19 | + | 
 | 20 | +# Configure logging  | 
 | 21 | +logging.basicConfig(  | 
 | 22 | +    level=logging.INFO,  | 
 | 23 | +    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'  | 
 | 24 | +)  | 
 | 25 | +log = logging.getLogger(__name__)  | 
 | 26 | + | 
 | 27 | +# ========== CONFIGURATION ==========  | 
 | 28 | +MODEL = "MESSAGE_GLOBIOM_SSP2_v6.1"  | 
 | 29 | +BASE_SCEN = "baseline"  | 
 | 30 | +N_TIME = 12  # 12 monthly timeslices  | 
 | 31 | +REGIONS = "R12"  | 
 | 32 | + | 
 | 33 | +# Nexus configuration  | 
 | 34 | +RCP = "7p0"  | 
 | 35 | +SDG = "baseline"  | 
 | 36 | +REL = "low"  | 
 | 37 | +SSP = "SSP2"  | 
 | 38 | + | 
 | 39 | +# Basin filtering for reduced nexus  | 
 | 40 | +REDUCED_BASIN = True  | 
 | 41 | +FILTER_LIST = [  | 
 | 42 | +    "10|SAS",  | 
 | 43 | +    "115|SAS",  | 
 | 44 | +    "124|SAS",  | 
 | 45 | +    "12|SAS",  | 
 | 46 | +    "141|SAS",  | 
 | 47 | +    "148|SAS",  | 
 | 48 | +    "15|SAS",  | 
 | 49 | +    "24|SAS",  | 
 | 50 | +    "30|SAS",  | 
 | 51 | +    "50|SAS",  | 
 | 52 | +    "53|SAS",  | 
 | 53 | +    "65|SAS",  | 
 | 54 | +    "66|SAS",  | 
 | 55 | +    "67|SAS",  | 
 | 56 | +    "70|SAS",  | 
 | 57 | +]  | 
 | 58 | + | 
 | 59 | +# Timeslice configuration  | 
 | 60 | +REMOVE_COOLING = True  # Remove cooling techs before adding timeslices (water module adds them back)  | 
 | 61 | +UPDATE_RESERVE_MARGIN = False  # Set to True if you have peak_demand data in Excel  | 
 | 62 | + | 
 | 63 | +# Solve configuration  | 
 | 64 | +SOLVE_OPTIONS = {  | 
 | 65 | +    "lpmethod": "4",  | 
 | 66 | +    "scaind": "-1",  | 
 | 67 | +    "threads": "16",  | 
 | 68 | +    "iis": "1"  | 
 | 69 | +}  | 
 | 70 | + | 
 | 71 | +def main():  | 
 | 72 | +    """Execute the full subannual nexus pipeline."""  | 
 | 73 | + | 
 | 74 | +    # ========== STEP 1: Load Base Scenario ==========  | 
 | 75 | +    log.info("=" * 80)  | 
 | 76 | +    log.info("STEP 1: Loading base scenario")  | 
 | 77 | +    log.info("=" * 80)  | 
 | 78 | + | 
 | 79 | +    ctx = Context()  | 
 | 80 | +    ctx.handle_cli_args(url=f"ixmp://ixmp_dev/{MODEL}/{BASE_SCEN}")  | 
 | 81 | + | 
 | 82 | +    sc_base = ctx.get_scenario()  | 
 | 83 | +    log.info(f"Loaded base scenario: {sc_base.model}/{sc_base.scenario} v{sc_base.version}")  | 
 | 84 | + | 
 | 85 | +    # ========== STEP 2: Clone and Add Timeslices ==========  | 
 | 86 | +    log.info("=" * 80)  | 
 | 87 | +    log.info(f"STEP 2: Adding {N_TIME} monthly timeslices")  | 
 | 88 | +    log.info("=" * 80)  | 
 | 89 | + | 
 | 90 | +    sc_timeslice = sc_base.clone(  | 
 | 91 | +        model=MODEL,  | 
 | 92 | +        scenario=f"{BASE_SCEN}_t{N_TIME}",  | 
 | 93 | +        keep_solution=False  | 
 | 94 | +    )  | 
 | 95 | +    log.info(f"Cloned to: {sc_timeslice.model}/{sc_timeslice.scenario} v{sc_timeslice.version}")  | 
 | 96 | + | 
 | 97 | +    # Add timeslices  | 
 | 98 | +    ctx.regions = REGIONS  | 
 | 99 | +    sc_timeslice = add_timeslices(  | 
 | 100 | +        scenario=sc_timeslice,  | 
 | 101 | +        context=ctx,  | 
 | 102 | +        n_time=N_TIME,  | 
 | 103 | +        regions=REGIONS,  | 
 | 104 | +        remove_cooling_tec=REMOVE_COOLING,  | 
 | 105 | +        update_reserve_margin=UPDATE_RESERVE_MARGIN,  | 
 | 106 | +    )  | 
 | 107 | + | 
 | 108 | +    log.info(f"Timeslices added successfully")  | 
 | 109 | + | 
 | 110 | +    # Verify time structure  | 
 | 111 | +    times = sc_timeslice.set("time")  | 
 | 112 | +    log.info(f"Time slices in scenario: {list(times)}")  | 
 | 113 | + | 
 | 114 | +    # ========== STEP 3: Validate Timesliced Scenario ==========  | 
 | 115 | +    log.info("=" * 80)  | 
 | 116 | +    log.info("STEP 3: Validating timesliced scenario (optional solve)")  | 
 | 117 | +    log.info("=" * 80)  | 
 | 118 | + | 
 | 119 | +    validate_timeslice = input("Solve timesliced scenario to validate? (y/n): ").lower()  | 
 | 120 | +    if validate_timeslice == 'y':  | 
 | 121 | +        log.info("Solving timesliced scenario...")  | 
 | 122 | +        sc_timeslice.set_as_default()  | 
 | 123 | +        try:  | 
 | 124 | +            sc_timeslice.solve(solve_options=SOLVE_OPTIONS)  | 
 | 125 | +            log.info("Timesliced scenario solved successfully!")  | 
 | 126 | +        except Exception as e:  | 
 | 127 | +            log.error(f"Failed to solve timesliced scenario: {e}")  | 
 | 128 | +            log.error("Continuing anyway to build nexus structure...")  | 
 | 129 | +    else:  | 
 | 130 | +        log.info("Skipping validation solve")  | 
 | 131 | + | 
 | 132 | +    # ========== STEP 4: Initialize Water Context ==========  | 
 | 133 | +    log.info("=" * 80)  | 
 | 134 | +    log.info("STEP 4: Initializing water/nexus context")  | 
 | 135 | +    log.info("=" * 80)  | 
 | 136 | + | 
 | 137 | +    ctx.ssp = SSP  | 
 | 138 | +    water_ini(ctx, regions=REGIONS, time=None)  # Will auto-detect monthly timeslices  | 
 | 139 | + | 
 | 140 | +    log.info(f"Detected time structure: {ctx.time}")  | 
 | 141 | + | 
 | 142 | +    # Configure nexus  | 
 | 143 | +    ctx.nexus_set = "nexus"  | 
 | 144 | +    ctx.RCP = RCP  | 
 | 145 | +    ctx.SDG = SDG  | 
 | 146 | +    ctx.REL = REL  | 
 | 147 | +    ctx.reduced_basin = REDUCED_BASIN  | 
 | 148 | +    ctx.filter_list = FILTER_LIST  | 
 | 149 | + | 
 | 150 | +    log.info(f"Nexus configuration:")  | 
 | 151 | +    log.info(f"  SSP: {ctx.ssp}")  | 
 | 152 | +    log.info(f"  RCP: {ctx.RCP}")  | 
 | 153 | +    log.info(f"  SDG: {ctx.SDG}")  | 
 | 154 | +    log.info(f"  REL: {ctx.REL}")  | 
 | 155 | +    log.info(f"  Reduced basins: {ctx.reduced_basin}")  | 
 | 156 | +    if ctx.reduced_basin:  | 
 | 157 | +        log.info(f"  Filtered basins: {len(ctx.filter_list)} basins")  | 
 | 158 | + | 
 | 159 | +    # ========== STEP 5: Build Nexus on Timesliced Scenario ==========  | 
 | 160 | +    log.info("=" * 80)  | 
 | 161 | +    log.info("STEP 5: Building nexus module on timesliced scenario")  | 
 | 162 | +    log.info("=" * 80)  | 
 | 163 | + | 
 | 164 | +    sc_nexus = sc_timeslice.clone(  | 
 | 165 | +        model=MODEL,  | 
 | 166 | +        scenario=f"{BASE_SCEN}_t{N_TIME}_nexus_reduced",  | 
 | 167 | +        keep_solution=False  | 
 | 168 | +    )  | 
 | 169 | +    log.info(f"Cloned to: {sc_nexus.model}/{sc_nexus.scenario} v{sc_nexus.version}")  | 
 | 170 | + | 
 | 171 | +    log.info("Adding nexus structure and data...")  | 
 | 172 | +    build_nexus(ctx, sc_nexus)  | 
 | 173 | +    log.info("Nexus build completed")  | 
 | 174 | + | 
 | 175 | +    # ========== STEP 6: Solve Final Model ==========  | 
 | 176 | +    log.info("=" * 80)  | 
 | 177 | +    log.info("STEP 6: Solving final subannual nexus model")  | 
 | 178 | +    log.info("=" * 80)  | 
 | 179 | + | 
 | 180 | +    sc_nexus.set_as_default()  | 
 | 181 | + | 
 | 182 | +    log.info(f"Scenario: {sc_nexus.model}/{sc_nexus.scenario} v{sc_nexus.version}")  | 
 | 183 | +    log.info(f"Solve options: {SOLVE_OPTIONS}")  | 
 | 184 | + | 
 | 185 | +    proceed = input("Proceed with solve? (y/n): ").lower()  | 
 | 186 | +    if proceed == 'y':  | 
 | 187 | +        log.info("Starting solve...")  | 
 | 188 | +        try:  | 
 | 189 | +            sc_nexus.solve(solve_options=SOLVE_OPTIONS)  | 
 | 190 | +            log.info("=" * 80)  | 
 | 191 | +            log.info("SUCCESS! Subannual nexus model solved successfully")  | 
 | 192 | +            log.info("=" * 80)  | 
 | 193 | +            log.info(f"Final scenario: {sc_nexus.model}/{sc_nexus.scenario} v{sc_nexus.version}")  | 
 | 194 | +        except Exception as e:  | 
 | 195 | +            log.error(f"Solve failed: {e}")  | 
 | 196 | +            log.error("Check solver logs for details")  | 
 | 197 | +            raise  | 
 | 198 | +    else:  | 
 | 199 | +        log.info("Solve skipped by user")  | 
 | 200 | +        log.info(f"Scenario ready for solving: {sc_nexus.model}/{sc_nexus.scenario} v{sc_nexus.version}")  | 
 | 201 | + | 
 | 202 | +    return sc_nexus  | 
 | 203 | + | 
 | 204 | + | 
 | 205 | +if __name__ == "__main__":  | 
 | 206 | +    try:  | 
 | 207 | +        scenario = main()  | 
 | 208 | +        print("\n" + "=" * 80)  | 
 | 209 | +        print("Pipeline completed successfully!")  | 
 | 210 | +        print("=" * 80)  | 
 | 211 | +    except Exception as e:  | 
 | 212 | +        log.error(f"Pipeline failed: {e}", exc_info=True)  | 
 | 213 | +        raise  | 
0 commit comments