flextool 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flextool/__init__.py +41 -0
- flextool/_mem_sampler.py +193 -0
- flextool/_resources.py +43 -0
- flextool/calibrate/__init__.py +51 -0
- flextool/calibrate/__main__.py +11 -0
- flextool/calibrate/_cli.py +316 -0
- flextool/calibrate/_db_alt.py +166 -0
- flextool/calibrate/_final_outputs.py +110 -0
- flextool/calibrate/_guard.py +151 -0
- flextool/calibrate/_loop.py +558 -0
- flextool/calibrate/_readers.py +223 -0
- flextool/calibrate/_report.py +263 -0
- flextool/calibrate/_sizing.py +699 -0
- flextool/calibrate/_solve.py +134 -0
- flextool/calibrate/_solve_status.py +495 -0
- flextool/cli/__init__.py +9 -0
- flextool/cli/_console.py +51 -0
- flextool/cli/_timing.py +147 -0
- flextool/cli/cmd_execute_flextool_workflow.py +187 -0
- flextool/cli/cmd_export_to_tabular.py +56 -0
- flextool/cli/cmd_import_sensitivities.py +75 -0
- flextool/cli/cmd_migrate_database.py +13 -0
- flextool/cli/cmd_open_results_db.py +269 -0
- flextool/cli/cmd_read_matpower.py +66 -0
- flextool/cli/cmd_read_old_flextool.py +63 -0
- flextool/cli/cmd_read_self_describing_tabular_input.py +50 -0
- flextool/cli/cmd_read_tabular_input.py +81 -0
- flextool/cli/cmd_run_flextool.py +1095 -0
- flextool/cli/cmd_scenario_results.py +284 -0
- flextool/cli/cmd_solve_mps.py +169 -0
- flextool/cli/cmd_update_flextool.py +17 -0
- flextool/cli/cmd_write_outputs.py +125 -0
- flextool/common_utils/__init__.py +1 -0
- flextool/common_utils/plot_mem_shape.py +77 -0
- flextool/common_utils/precision.py +451 -0
- flextool/decomposition/__init__.py +0 -0
- flextool/decomposition/region_decomposition.py +128 -0
- flextool/decomposition/region_filter.py +1261 -0
- flextool/engine_polars/__init__.py +110 -0
- flextool/engine_polars/_axis_enums.py +742 -0
- flextool/engine_polars/_benders.py +3462 -0
- flextool/engine_polars/_block_layout.py +1479 -0
- flextool/engine_polars/_blocks.py +1515 -0
- flextool/engine_polars/_commodity_ladder.py +660 -0
- flextool/engine_polars/_cumulative_invest.py +1165 -0
- flextool/engine_polars/_db_loader.py +153 -0
- flextool/engine_polars/_db_reader.py +127 -0
- flextool/engine_polars/_dc_power_flow.py +445 -0
- flextool/engine_polars/_delay.py +442 -0
- flextool/engine_polars/_derived_arithmetic.py +432 -0
- flextool/engine_polars/_derived_block.py +990 -0
- flextool/engine_polars/_derived_branch.py +769 -0
- flextool/engine_polars/_derived_existing.py +1353 -0
- flextool/engine_polars/_derived_npv.py +1297 -0
- flextool/engine_polars/_derived_params.py +9850 -0
- flextool/engine_polars/_derived_profile.py +881 -0
- flextool/engine_polars/_derived_walks.py +276 -0
- flextool/engine_polars/_determinism.py +70 -0
- flextool/engine_polars/_direct_params.py +2186 -0
- flextool/engine_polars/_dump_csvs.py +1009 -0
- flextool/engine_polars/_emit_arc_unions.py +1631 -0
- flextool/engine_polars/_emit_calc_params.py +729 -0
- flextool/engine_polars/_emit_chain_params.py +709 -0
- flextool/engine_polars/_emit_co2_accumulators.py +400 -0
- flextool/engine_polars/_emit_dispatchers.py +690 -0
- flextool/engine_polars/_emit_energy_margin.py +125 -0
- flextool/engine_polars/_emit_energy_margin_adder.py +290 -0
- flextool/engine_polars/_emit_entity_annual.py +428 -0
- flextool/engine_polars/_emit_inflow_scaling.py +1420 -0
- flextool/engine_polars/_emit_leaf_sets.py +550 -0
- flextool/engine_polars/_emit_lp_scaling.py +665 -0
- flextool/engine_polars/_emit_mid_sets.py +859 -0
- flextool/engine_polars/_emit_pdt_params.py +759 -0
- flextool/engine_polars/_emit_per_solve.py +774 -0
- flextool/engine_polars/_emit_period_calc.py +504 -0
- flextool/engine_polars/_emit_period_params.py +2398 -0
- flextool/engine_polars/_emit_provider_io.py +141 -0
- flextool/engine_polars/_emit_reserve.py +574 -0
- flextool/engine_polars/_emit_solve_time.py +311 -0
- flextool/engine_polars/_emit_solve_writers.py +1249 -0
- flextool/engine_polars/_flex_data_accumulator.py +388 -0
- flextool/engine_polars/_flex_data_provider.py +478 -0
- flextool/engine_polars/_group_slack.py +1253 -0
- flextool/engine_polars/_inmemory_reader.py +140 -0
- flextool/engine_polars/_input_source.py +336 -0
- flextool/engine_polars/_invest_seeds.py +191 -0
- flextool/engine_polars/_native_input_writer.py +100 -0
- flextool/engine_polars/_native_run_model.py +1348 -0
- flextool/engine_polars/_orchestration.py +4314 -0
- flextool/engine_polars/_output_writer.py +439 -0
- flextool/engine_polars/_param_shapes.py +1595 -0
- flextool/engine_polars/_parquet_bundle.py +723 -0
- flextool/engine_polars/_pdt_join.py +167 -0
- flextool/engine_polars/_pdt_lookup.py +547 -0
- flextool/engine_polars/_per_solve_sets.py +335 -0
- flextool/engine_polars/_projection_params.py +2056 -0
- flextool/engine_polars/_provider_keys.py +173 -0
- flextool/engine_polars/_provider_translators.py +225 -0
- flextool/engine_polars/_recursive_solve.py +703 -0
- flextool/engine_polars/_region_filter.py +2508 -0
- flextool/engine_polars/_reserve.py +649 -0
- flextool/engine_polars/_solve_acceptance.py +331 -0
- flextool/engine_polars/_solve_config.py +1001 -0
- flextool/engine_polars/_solve_context.py +885 -0
- flextool/engine_polars/_solve_handoff.py +164 -0
- flextool/engine_polars/_solve_state.py +232 -0
- flextool/engine_polars/_solver_base.py +36 -0
- flextool/engine_polars/_solver_dispatch.py +511 -0
- flextool/engine_polars/_spinedb_reader.py +1165 -0
- flextool/engine_polars/_stochastic.py +593 -0
- flextool/engine_polars/_subprocess_solve.py +1838 -0
- flextool/engine_polars/_timeline.py +1416 -0
- flextool/engine_polars/_vectorize.py +438 -0
- flextool/engine_polars/_warm.py +858 -0
- flextool/engine_polars/autoscale/__init__.py +107 -0
- flextool/engine_polars/autoscale/_config.py +218 -0
- flextool/engine_polars/autoscale/_layer2.py +1253 -0
- flextool/engine_polars/autoscale/_layer2_types.py +584 -0
- flextool/engine_polars/autoscale/_quantity_types.py +621 -0
- flextool/engine_polars/autoscale/_report.py +336 -0
- flextool/engine_polars/chain.py +259 -0
- flextool/engine_polars/input.py +6638 -0
- flextool/engine_polars/model.py +4754 -0
- flextool/env_check.py +388 -0
- flextool/export_to_tabular/__init__.py +5 -0
- flextool/export_to_tabular/db_reader.py +224 -0
- flextool/export_to_tabular/excel_writer.py +3559 -0
- flextool/export_to_tabular/export_settings.yaml +377 -0
- flextool/export_to_tabular/export_to_excel.py +227 -0
- flextool/export_to_tabular/formatting.py +543 -0
- flextool/export_to_tabular/sheet_config.py +876 -0
- flextool/gui/__init__.py +0 -0
- flextool/gui/__main__.py +118 -0
- flextool/gui/calibrate_commands.py +184 -0
- flextool/gui/calibrate_jobs.py +424 -0
- flextool/gui/check_tree.py +142 -0
- flextool/gui/cli_format.py +83 -0
- flextool/gui/config_parser.py +68 -0
- flextool/gui/data_models.py +362 -0
- flextool/gui/db_editor_integration.py +202 -0
- flextool/gui/db_version_check.py +269 -0
- flextool/gui/dialogs/__init__.py +0 -0
- flextool/gui/dialogs/add_dialog.py +1098 -0
- flextool/gui/dialogs/calibrate_dialog.py +1259 -0
- flextool/gui/dialogs/file_picker.py +473 -0
- flextool/gui/dialogs/group_picker.py +299 -0
- flextool/gui/dialogs/migration_consent_dialog.py +106 -0
- flextool/gui/dialogs/migration_progress_dialog.py +237 -0
- flextool/gui/dialogs/plot_dialog.py +459 -0
- flextool/gui/dialogs/plot_settings_picker.py +2184 -0
- flextool/gui/dialogs/project_dialog.py +426 -0
- flextool/gui/dialogs/update_dialog.py +212 -0
- flextool/gui/downsampling.py +88 -0
- flextool/gui/error_handling.py +50 -0
- flextool/gui/execution_manager.py +1715 -0
- flextool/gui/execution_window.py +1377 -0
- flextool/gui/hover_tooltip.py +111 -0
- flextool/gui/input_sources.py +730 -0
- flextool/gui/main_window.py +6181 -0
- flextool/gui/network_graph.py +215 -0
- flextool/gui/output_actions.py +393 -0
- flextool/gui/output_log_window.py +159 -0
- flextool/gui/platform_utils.py +421 -0
- flextool/gui/plot_cache.py +88 -0
- flextool/gui/plot_canvas.py +543 -0
- flextool/gui/plot_config_reader.py +272 -0
- flextool/gui/project_utils.py +100 -0
- flextool/gui/result_viewer.py +4394 -0
- flextool/gui/scenario_key.py +162 -0
- flextool/gui/scenario_lists.py +516 -0
- flextool/gui/settings_io.py +360 -0
- flextool/gui/solve_reader.py +103 -0
- flextool/gui/tree_reorder.py +88 -0
- flextool/gui/ui_metrics.py +420 -0
- flextool/input_derivation/__init__.py +281 -0
- flextool/input_derivation/_commodity_ladder.py +375 -0
- flextool/input_derivation/_commodity_ladder_sets.py +70 -0
- flextool/input_derivation/_dc_power_flow.py +377 -0
- flextool/input_derivation/_method_constants.py +77 -0
- flextool/input_derivation/_process_method.py +258 -0
- flextool/input_derivation/_specs.py +1026 -0
- flextool/input_derivation/_validators.py +321 -0
- flextool/lean_parquet.py +159 -0
- flextool/model_builder/__init__.py +5 -0
- flextool/model_builder/build_model.py +589 -0
- flextool/model_builder/encoding.py +67 -0
- flextool/model_builder/names.py +34 -0
- flextool/model_builder/profiles.py +129 -0
- flextool/plot_outputs/__init__.py +14 -0
- flextool/plot_outputs/axis_helpers.py +355 -0
- flextool/plot_outputs/color_template.py +888 -0
- flextool/plot_outputs/config.py +171 -0
- flextool/plot_outputs/format_helpers.py +345 -0
- flextool/plot_outputs/legend_helpers.py +143 -0
- flextool/plot_outputs/orchestrator.py +1141 -0
- flextool/plot_outputs/perf.py +37 -0
- flextool/plot_outputs/plan.py +1787 -0
- flextool/plot_outputs/plot_bars.py +1510 -0
- flextool/plot_outputs/plot_bars_detail.py +753 -0
- flextool/plot_outputs/plot_lines.py +951 -0
- flextool/plot_outputs/shared_manifest.py +564 -0
- flextool/plot_outputs/subplot_helpers.py +137 -0
- flextool/process_inputs/__init__.py +188 -0
- flextool/process_inputs/import_old_excel_input.json +4159 -0
- flextool/process_inputs/read_matpower.py +451 -0
- flextool/process_inputs/read_old_flextool.py +1288 -0
- flextool/process_inputs/read_self_describing_excel.py +1423 -0
- flextool/process_inputs/read_tabular_with_specification.py +1114 -0
- flextool/process_inputs/write_old_flextool_to_db.py +3077 -0
- flextool/process_inputs/write_self_describing_to_db.py +977 -0
- flextool/process_inputs/write_to_input_db.py +269 -0
- flextool/process_outputs/__init__.py +7 -0
- flextool/process_outputs/_annualize.py +55 -0
- flextool/process_outputs/_inmemory_helpers.py +292 -0
- flextool/process_outputs/_output_meta.py +672 -0
- flextool/process_outputs/calc_capacity_flows.py +107 -0
- flextool/process_outputs/calc_connections.py +136 -0
- flextool/process_outputs/calc_costs.py +260 -0
- flextool/process_outputs/calc_group_flows.py +192 -0
- flextool/process_outputs/calc_slacks.py +103 -0
- flextool/process_outputs/calc_storage_vre.py +160 -0
- flextool/process_outputs/drop_levels.py +208 -0
- flextool/process_outputs/handoff_writers.py +1315 -0
- flextool/process_outputs/out_ancillary.py +544 -0
- flextool/process_outputs/out_capacity.py +179 -0
- flextool/process_outputs/out_costs.py +334 -0
- flextool/process_outputs/out_flowgroup.py +189 -0
- flextool/process_outputs/out_flows.py +301 -0
- flextool/process_outputs/out_group.py +475 -0
- flextool/process_outputs/out_node.py +190 -0
- flextool/process_outputs/persist_realized_slice.py +601 -0
- flextool/process_outputs/process_results.py +24 -0
- flextool/process_outputs/read_highs_solution.py +2256 -0
- flextool/process_outputs/read_parameters.py +1799 -0
- flextool/process_outputs/read_sets.py +1095 -0
- flextool/process_outputs/read_variables.py +553 -0
- flextool/process_outputs/solve_order.py +81 -0
- flextool/process_outputs/spinedb_replay.py +412 -0
- flextool/process_outputs/union_realized_slice.py +224 -0
- flextool/process_outputs/write_outputs.py +1286 -0
- flextool/process_outputs/write_spinedb.py +1267 -0
- flextool/representative_periods/__init__.py +5 -0
- flextool/representative_periods/clustering.py +165 -0
- flextool/representative_periods/force_include.py +563 -0
- flextool/representative_periods/netload.py +365 -0
- flextool/representative_periods/netload_inputs.py +345 -0
- flextool/representative_periods/netload_iterate.py +722 -0
- flextool/representative_periods/preprocess.py +948 -0
- flextool/representative_periods/scenario_stack.py +195 -0
- flextool/representative_periods/weights.py +124 -0
- flextool/scenario_comparison/__init__.py +13 -0
- flextool/scenario_comparison/config_builder.py +158 -0
- flextool/scenario_comparison/constants.py +20 -0
- flextool/scenario_comparison/data_models.py +222 -0
- flextool/scenario_comparison/db_reader.py +399 -0
- flextool/scenario_comparison/dispatch_data.py +1002 -0
- flextool/scenario_comparison/dispatch_mappings.py +205 -0
- flextool/scenario_comparison/dispatch_plots.py +691 -0
- flextool/scenario_comparison/input_entity_colors.py +319 -0
- flextool/scenario_comparison/orchestrator.py +453 -0
- flextool/scenario_comparison/plan_union.py +244 -0
- flextool/scenario_comparison/plot_settings_seed.py +205 -0
- flextool/schemas/AXIS_CONTRACT.md +71 -0
- flextool/schemas/canonical_databases/howto_aggregate_output.json +6225 -0
- flextool/schemas/canonical_databases/howto_connections.json +5606 -0
- flextool/schemas/canonical_databases/howto_demand.json +5518 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir.json +6239 -0
- flextool/schemas/canonical_databases/howto_hydro_reservoir_with_pump.json +5933 -0
- flextool/schemas/canonical_databases/howto_non_sync_and_curtailment.json +5794 -0
- flextool/schemas/canonical_databases/howto_ramp_and_start_up.json +5707 -0
- flextool/schemas/canonical_databases/howto_stochastics.json +6032 -0
- flextool/schemas/canonical_databases/templates_examples.json +13532 -0
- flextool/schemas/canonical_databases/templates_time_settings_only.json +5340 -0
- flextool/schemas/comparison_settings_template.json +197 -0
- flextool/schemas/default_plot_settings.yaml +260 -0
- flextool/schemas/default_plots.yaml +2293 -0
- flextool/schemas/flextool_axis_contract.json +303 -0
- flextool/schemas/flextool_axis_contract.schema.json +247 -0
- flextool/schemas/old_flextool_import_template.json +4443 -0
- flextool/schemas/output_info_template.json +48 -0
- flextool/schemas/output_settings_template.json +256 -0
- flextool/schemas/pre_v26/flextool_template_constant_default.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_default_optional_output.json +2152 -0
- flextool/schemas/pre_v26/flextool_template_default_value.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_drop_down.json +2080 -0
- flextool/schemas/pre_v26/flextool_template_lifetime_method.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_optional_outputs.json +2094 -0
- flextool/schemas/pre_v26/flextool_template_output_node_flows.json +2105 -0
- flextool/schemas/pre_v26/flextool_template_results_master.json +493 -0
- flextool/schemas/pre_v26/flextool_template_rolling_start_remove.json +2087 -0
- flextool/schemas/pre_v26/flextool_template_rolling_window.json +2059 -0
- flextool/schemas/pre_v26/flextool_template_storage_binding_defaults.json +46 -0
- flextool/schemas/pre_v26/flextool_template_v2.json +1990 -0
- flextool/schemas/pre_v26/flextool_template_v25.json +3864 -0
- flextool/schemas/spinedb_results_schema.json +581 -0
- flextool/schemas/spinedb_schema.json +4636 -0
- flextool/solver_config/copt.opt.template +18 -0
- flextool/solver_config/cplex.opt.template +25 -0
- flextool/solver_config/gurobi.opt.template +18 -0
- flextool/solver_config/highs.opt.template +18 -0
- flextool/solver_config/xpress.opt.template +26 -0
- flextool/spinedb_backend/__init__.py +26 -0
- flextool/spinedb_backend/_axis_enums.py +1119 -0
- flextool/spinedb_backend/_backend.py +1139 -0
- flextool/update_flextool/__init__.py +12 -0
- flextool/update_flextool/canonical_databases.py +251 -0
- flextool/update_flextool/db_migration.py +7108 -0
- flextool/update_flextool/ensure_settings_db.py +138 -0
- flextool/update_flextool/export_database.py +103 -0
- flextool/update_flextool/extend_tests_fixture.py +772 -0
- flextool/update_flextool/generate_canonical.py +274 -0
- flextool/update_flextool/initialize_database.py +42 -0
- flextool/update_flextool/install_info.py +225 -0
- flextool/update_flextool/self_update.py +464 -0
- flextool/update_flextool/sync_master_json_template.py +125 -0
- flextool/update_flextool/test_fixtures.py +187 -0
- flextool-4.0.0.dist-info/METADATA +217 -0
- flextool-4.0.0.dist-info/RECORD +322 -0
- flextool-4.0.0.dist-info/WHEEL +5 -0
- flextool-4.0.0.dist-info/entry_points.txt +17 -0
- flextool-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- flextool-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
"""Cluster A — annual integration / NPV (Δ.5).
|
|
2
|
+
|
|
3
|
+
Lazy-polars port of flextool's NPV / lifetime-fixed-cost / inflation
|
|
4
|
+
cascade. Replaces the eager-Python implementation that previously lived
|
|
5
|
+
in :mod:`._derived_params` (``ed_entity_annual_family_from_source`` plus
|
|
6
|
+
its supporting ``_per_method_annuity`` / ``_resolve_pdX`` helpers).
|
|
7
|
+
|
|
8
|
+
The cluster covers six FlexData fields:
|
|
9
|
+
|
|
10
|
+
* ``p_inflation_op`` — per-period operations
|
|
11
|
+
inflation factor. Replaces the workdir read of
|
|
12
|
+
``solve_data/p_inflation_factor_operations_yearly.csv``.
|
|
13
|
+
* ``p_ed_fixed_cost`` — per-entity per-period
|
|
14
|
+
raw fixed cost (× 1000 for nodes/processes). Replaces the workdir
|
|
15
|
+
read of ``solve_data/ed_fixed_cost.csv``.
|
|
16
|
+
* ``ed_entity_annual_discounted`` — invest-side NPV.
|
|
17
|
+
Replaces ``solve_data/ed_entity_annual_discounted.csv``.
|
|
18
|
+
* ``ed_entity_annual_divest_discounted`` — divest-side NPV.
|
|
19
|
+
Replaces ``solve_data/ed_entity_annual_divest_discounted.csv``.
|
|
20
|
+
* ``ed_lifetime_fixed_cost`` — invest-side lifetime
|
|
21
|
+
cumulative fixed cost. Replaces ``solve_data/ed_lifetime_fixed_cost.csv``.
|
|
22
|
+
* ``ed_lifetime_fixed_cost_divest`` — divest-side lifetime
|
|
23
|
+
cumulative fixed cost. Replaces ``solve_data/ed_lifetime_fixed_cost_divest.csv``.
|
|
24
|
+
|
|
25
|
+
Architecture
|
|
26
|
+
------------
|
|
27
|
+
|
|
28
|
+
Per the user-locked decisions for derived clusters (audit handoff at
|
|
29
|
+
Δ.5):
|
|
30
|
+
|
|
31
|
+
1. **Lazy polars throughout.** Helpers return ``pl.LazyFrame`` chains;
|
|
32
|
+
the public entry points materialise via ``Param(...)`` at the
|
|
33
|
+
``apply_derived_f`` boundary.
|
|
34
|
+
2. **Convention over typed BuildPipeline.** The dependency graph is
|
|
35
|
+
shallow: ``period_walk_iterator`` → 4 NPV helpers; ``p_ed_fixed_cost``
|
|
36
|
+
feeds the 2 lifetime-fixed-cost helpers. Plain function calls.
|
|
37
|
+
3. **4 NPV variants split into 4 helpers** sharing
|
|
38
|
+
:func:`period_walk_iterator` for the per-period accumulation walk.
|
|
39
|
+
|
|
40
|
+
Algorithm:
|
|
41
|
+
|
|
42
|
+
For each (entity e, period d) in the relevant invest/divest/all-entity
|
|
43
|
+
domain, the NPV value is::
|
|
44
|
+
|
|
45
|
+
annuity[e, d] × Σ_{d_all ∈ period_in_use, window(d, d_all, e)}
|
|
46
|
+
inflation_factor[d_all]
|
|
47
|
+
|
|
48
|
+
where:
|
|
49
|
+
|
|
50
|
+
* ``annuity`` = sum-over-allowed-methods of
|
|
51
|
+
``invest_value × 1000 × r / (1 - (1/(1+r))^n)`` with
|
|
52
|
+
``r ≤ 0 → 0.05`` and ``n ≤ 0 → 20`` fallbacks.
|
|
53
|
+
* ``window(d, d_all, e)``:
|
|
54
|
+
- ``reinvest_choice`` / ``no_investment``:
|
|
55
|
+
``pdy[d_all] ∈ [pdy[d], pdy[d] + lifetime[e, d])``.
|
|
56
|
+
- ``reinvest_automatic``: ``pdy[d_all] ≥ pdy[d]``.
|
|
57
|
+
- divest side: always the bounded form using raw ``lifetime``.
|
|
58
|
+
* ``inflation_factor[d_all]``:
|
|
59
|
+
- investment side → ``p_inflation_factor_investment_yearly[d_all]``.
|
|
60
|
+
- operations side (``ed_lifetime_fixed_cost`` only) →
|
|
61
|
+
``p_inflation_factor_operations_yearly[d_all]``.
|
|
62
|
+
- ``ed_lifetime_fixed_cost_divest`` is asymmetric and uses the
|
|
63
|
+
investment factor (mirrors flextool.mod L1651).
|
|
64
|
+
|
|
65
|
+
The annuity formula and the discount-window walk are 1:1 ports of the
|
|
66
|
+
flextool source; deviations are documented inline.
|
|
67
|
+
|
|
68
|
+
Cross-references
|
|
69
|
+
----------------
|
|
70
|
+
|
|
71
|
+
* :mod:`._derived_params._inflation_yearly_from_source` — the yearly
|
|
72
|
+
inflation cascade computation used to be eager-Python; we keep that
|
|
73
|
+
helper's algorithm but expose a lazy-frame variant
|
|
74
|
+
(:func:`_inflation_factors_lf`) for use here.
|
|
75
|
+
* :func:`._derived_params._p_years_d_lf` — already lazy; reused as-is.
|
|
76
|
+
* :func:`._derived_params._period_in_use_set` and
|
|
77
|
+
:func:`._derived_params._solve_periods` — list builders used to
|
|
78
|
+
bound the period domain.
|
|
79
|
+
"""
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
from typing import TYPE_CHECKING
|
|
84
|
+
|
|
85
|
+
import polars as pl
|
|
86
|
+
|
|
87
|
+
from polar_high import Param
|
|
88
|
+
|
|
89
|
+
from ._axis_enums import (
|
|
90
|
+
alias_to_axis,
|
|
91
|
+
schema_dtype,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Substrate handle for the cascade-wide axis enum vocabulary.
|
|
95
|
+
# Bare ``None`` here; ``cast_dim`` / ``schema_dtype`` in
|
|
96
|
+
# ``_axis_enums`` fall back to ``_LIVE_AXIS_ENUMS_CTX`` (the live
|
|
97
|
+
# ContextVar) when this is ``None``, so substrate sites pick up
|
|
98
|
+
# activation set by ``load_flextool`` automatically.
|
|
99
|
+
_enums: "dict | None" = None
|
|
100
|
+
|
|
101
|
+
if TYPE_CHECKING:
|
|
102
|
+
from flextool.engine_polars._input_source import InputSource
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Mirror flextool/flextool_base.dat:211-212 (cf. entity_annual_calc_params.py).
|
|
106
|
+
_INVEST_NOT_ALLOWED: frozenset[str] = frozenset((
|
|
107
|
+
"not_allowed", "retire_period", "retire_total", "retire_no_limit",
|
|
108
|
+
))
|
|
109
|
+
_DIVEST_NOT_ALLOWED: frozenset[str] = frozenset((
|
|
110
|
+
"not_allowed", "invest_period", "invest_total", "invest_no_limit",
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
# Default lifetime_method when an entity has no explicit row
|
|
114
|
+
# (preprocessing/method_with_fallback_sets.py::_LIFETIME_METHOD_DEFAULT).
|
|
115
|
+
_LIFETIME_METHOD_DEFAULT = "reinvest_automatic"
|
|
116
|
+
|
|
117
|
+
# Inflation defaults — mirrors period_calculated_params.py:221-227 and
|
|
118
|
+
# _solve_inflation_inputs in _derived_params.py.
|
|
119
|
+
_INFL_RATE_DEFAULT = 0.0
|
|
120
|
+
_INFL_OFFSET_INV_DEFAULT = 0.0
|
|
121
|
+
_INFL_OFFSET_OPS_DEFAULT = 0.5
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Building blocks (lazy frames)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _entity_class_lf(source: "InputSource") -> pl.LazyFrame:
|
|
130
|
+
"""Lazy ``(e, ec)`` frame: every (entity, entity_class) pairing.
|
|
131
|
+
|
|
132
|
+
Entity classes: ``unit``, ``node``, ``connection``. ``unit ∪
|
|
133
|
+
connection`` forms flextool's ``process_set``; ``node`` is its own
|
|
134
|
+
class. The frame supports the per-entity-class branching that
|
|
135
|
+
``_resolve_pdX`` does in eager Python.
|
|
136
|
+
"""
|
|
137
|
+
parts: list[pl.LazyFrame] = []
|
|
138
|
+
for ec in ("unit", "node", "connection"):
|
|
139
|
+
try:
|
|
140
|
+
df = source.entities(ec)
|
|
141
|
+
except KeyError:
|
|
142
|
+
continue
|
|
143
|
+
if df.height == 0:
|
|
144
|
+
continue
|
|
145
|
+
parts.append(df.lazy().select(
|
|
146
|
+
alias_to_axis("name", "e"),
|
|
147
|
+
pl.lit(ec).alias("ec"),
|
|
148
|
+
))
|
|
149
|
+
if not parts:
|
|
150
|
+
return pl.LazyFrame(schema={"e": schema_dtype(_enums, "e"),
|
|
151
|
+
"ec": pl.Utf8})
|
|
152
|
+
return pl.concat(parts, how="vertical")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _entity_class_lookup_lf(entity_class_lf: pl.LazyFrame) -> pl.LazyFrame:
|
|
156
|
+
"""Lazy ``(e, is_process, is_node)`` frame: bool flags per entity.
|
|
157
|
+
|
|
158
|
+
``is_process = ec ∈ {unit, connection}``, ``is_node = ec == 'node'``.
|
|
159
|
+
"""
|
|
160
|
+
return (entity_class_lf
|
|
161
|
+
.with_columns(
|
|
162
|
+
is_process=pl.col("ec").is_in(["unit", "connection"]),
|
|
163
|
+
is_node=pl.col("ec") == "node",
|
|
164
|
+
)
|
|
165
|
+
.group_by("e")
|
|
166
|
+
.agg(
|
|
167
|
+
pl.col("is_process").any().alias("is_process"),
|
|
168
|
+
pl.col("is_node").any().alias("is_node"),
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _per_entity_param_lf(source: "InputSource",
|
|
173
|
+
parameter_name: str,
|
|
174
|
+
) -> pl.LazyFrame:
|
|
175
|
+
"""Lazy ``(e, d, value)`` frame from ``unit/node/connection.<param>``.
|
|
176
|
+
|
|
177
|
+
Mirrors :func:`._derived_params._resolve_pdX` semantics in lazy form:
|
|
178
|
+
|
|
179
|
+
* Per-period (Map) rows produce ``(e, d, value)`` rows directly.
|
|
180
|
+
* Scalar rows produce ``(e, value)`` rows with ``d`` null;
|
|
181
|
+
consumers cross-join with the period universe.
|
|
182
|
+
|
|
183
|
+
Returns columns ``[e, d, value, is_scalar]`` — ``is_scalar`` flags
|
|
184
|
+
rows that need broadcasting. ``d`` is null on scalar rows.
|
|
185
|
+
"""
|
|
186
|
+
parts: list[pl.LazyFrame] = []
|
|
187
|
+
for ec in ("unit", "node", "connection"):
|
|
188
|
+
try:
|
|
189
|
+
df = source.parameter(ec, parameter_name)
|
|
190
|
+
except KeyError:
|
|
191
|
+
continue
|
|
192
|
+
if df.height == 0:
|
|
193
|
+
continue
|
|
194
|
+
cols = df.columns
|
|
195
|
+
# Period-dim detection — see the matching helper in
|
|
196
|
+
# ``_derived_existing._per_entity_param_lf``. When the source
|
|
197
|
+
# exposes the period axis under a user-renamed ``Map.index_name``
|
|
198
|
+
# (e.g. ``x``), checking only ``"period" in cols`` misses it and
|
|
199
|
+
# the scalar-broadcast branch explodes per-period rows through
|
|
200
|
+
# ``_resolve_per_period_lf``'s ``scalar.join(on="e")``.
|
|
201
|
+
extra = [c for c in cols if c not in ("name", "value")]
|
|
202
|
+
period_col = "period" if "period" in extra else (extra[0] if extra else None)
|
|
203
|
+
if period_col is not None:
|
|
204
|
+
# Per-ROW scalar detection. A scalar value stored alongside
|
|
205
|
+
# Map values in the same entity class surfaces with a NULL
|
|
206
|
+
# index ((e.g. ``x = null``) — Spine emits one ``x`` column
|
|
207
|
+
# for the whole class, so a unit with a constant ``existing``
|
|
208
|
+
# while a sibling unit carries a period-Map shows up as a
|
|
209
|
+
# null-period row. Such rows must broadcast across the
|
|
210
|
+
# period universe (``is_scalar=True``), NOT be treated as an
|
|
211
|
+
# explicit ``(e, d=null)`` row that fails to join the period
|
|
212
|
+
# grid and silently fills 0. Classifying by ``period_col``'s
|
|
213
|
+
# null-ness per row (rather than per frame) covers the mixed
|
|
214
|
+
# scalar+Map class.
|
|
215
|
+
parts.append(df.lazy().select(
|
|
216
|
+
alias_to_axis("name", "e"),
|
|
217
|
+
alias_to_axis(pl.col(period_col).cast(pl.Utf8, strict=False),
|
|
218
|
+
"d"),
|
|
219
|
+
pl.col("value").cast(pl.Float64, strict=False),
|
|
220
|
+
pl.col(period_col).is_null().alias("is_scalar"),
|
|
221
|
+
))
|
|
222
|
+
else:
|
|
223
|
+
parts.append(df.lazy().select(
|
|
224
|
+
alias_to_axis("name", "e"),
|
|
225
|
+
pl.lit(None).cast(schema_dtype(_enums, "d"), strict=False).alias("d"),
|
|
226
|
+
pl.col("value").cast(pl.Float64, strict=False),
|
|
227
|
+
pl.lit(True).alias("is_scalar"),
|
|
228
|
+
))
|
|
229
|
+
if not parts:
|
|
230
|
+
return pl.LazyFrame(schema={
|
|
231
|
+
"e": schema_dtype(_enums, "e"),
|
|
232
|
+
"d": schema_dtype(_enums, "d"),
|
|
233
|
+
"value": pl.Float64, "is_scalar": pl.Boolean,
|
|
234
|
+
})
|
|
235
|
+
return pl.concat(parts, how="vertical")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _resolve_per_period_lf(per_param: pl.LazyFrame,
|
|
239
|
+
ed_lf: pl.LazyFrame,
|
|
240
|
+
fill: float = 0.0,
|
|
241
|
+
) -> pl.LazyFrame:
|
|
242
|
+
"""Resolve ``pdX[e, param, d]`` cascade in lazy form.
|
|
243
|
+
|
|
244
|
+
Order (mirrors :func:`._derived_params._resolve_pdX`):
|
|
245
|
+
1. Explicit ``(e, d)`` row → take that ``value``.
|
|
246
|
+
2. Else scalar ``(e, ·)`` row → broadcast across ``ed_lf``.
|
|
247
|
+
3. Else → ``fill``.
|
|
248
|
+
|
|
249
|
+
Returns ``ed_lf`` with an extra ``value`` column (Float64).
|
|
250
|
+
``ed_lf`` must carry columns ``[e, d, ...]``.
|
|
251
|
+
"""
|
|
252
|
+
explicit = (per_param
|
|
253
|
+
.filter(~pl.col("is_scalar"))
|
|
254
|
+
.select("e", "d", pl.col("value").alias("v_explicit")))
|
|
255
|
+
scalar = (per_param
|
|
256
|
+
.filter(pl.col("is_scalar"))
|
|
257
|
+
.select("e", pl.col("value").alias("v_scalar")))
|
|
258
|
+
return (ed_lf
|
|
259
|
+
.join(explicit, on=["e", "d"], how="left")
|
|
260
|
+
.join(scalar, on="e", how="left")
|
|
261
|
+
.with_columns(
|
|
262
|
+
value=pl.coalesce(
|
|
263
|
+
pl.col("v_explicit"),
|
|
264
|
+
pl.col("v_scalar"),
|
|
265
|
+
pl.lit(fill, dtype=pl.Float64),
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
.drop("v_explicit", "v_scalar"))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _entity_method_lf(source: "InputSource",
|
|
272
|
+
parameter_name: str,
|
|
273
|
+
) -> pl.LazyFrame:
|
|
274
|
+
"""Lazy ``(e, method)`` frame from ``unit/node/connection.<param>``.
|
|
275
|
+
|
|
276
|
+
``parameter_name`` is one of ``invest_method`` or ``lifetime_method``.
|
|
277
|
+
Each entity may carry multiple method rows (Spine schema allows multi-
|
|
278
|
+
valued methods); we preserve all rows. The ``invest_method`` default
|
|
279
|
+
is ``not_allowed``; the ``lifetime_method`` default is
|
|
280
|
+
``reinvest_automatic`` — the latter is applied at the consumer site
|
|
281
|
+
(:func:`_lifetime_method_with_default_lf`).
|
|
282
|
+
"""
|
|
283
|
+
parts: list[pl.LazyFrame] = []
|
|
284
|
+
for ec in ("unit", "node", "connection"):
|
|
285
|
+
try:
|
|
286
|
+
df = source.parameter(ec, parameter_name)
|
|
287
|
+
except KeyError:
|
|
288
|
+
continue
|
|
289
|
+
if df.height == 0:
|
|
290
|
+
continue
|
|
291
|
+
parts.append(df.lazy().select(
|
|
292
|
+
alias_to_axis("name", "e"),
|
|
293
|
+
pl.col("value").cast(pl.Utf8, strict=False).alias("method"),
|
|
294
|
+
))
|
|
295
|
+
if not parts:
|
|
296
|
+
return pl.LazyFrame(schema={"e": schema_dtype(_enums, "e"),
|
|
297
|
+
"method": pl.Utf8})
|
|
298
|
+
return pl.concat(parts, how="vertical")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _lifetime_method_with_default_lf(
|
|
302
|
+
source: "InputSource",
|
|
303
|
+
all_entities_lf: pl.LazyFrame,
|
|
304
|
+
) -> pl.LazyFrame:
|
|
305
|
+
"""Lazy ``(e, method)`` frame for ``lifetime_method`` with default fill.
|
|
306
|
+
|
|
307
|
+
Entities without an explicit ``lifetime_method`` row get
|
|
308
|
+
``reinvest_automatic`` (mirrors
|
|
309
|
+
``preprocessing/method_with_fallback_sets.py::_LIFETIME_METHOD_DEFAULT``).
|
|
310
|
+
``all_entities_lf`` carries ``[e]``.
|
|
311
|
+
"""
|
|
312
|
+
explicit = _entity_method_lf(source, "lifetime_method")
|
|
313
|
+
explicit_entities = explicit.select("e").unique()
|
|
314
|
+
default_e = (all_entities_lf
|
|
315
|
+
.join(explicit_entities, on="e", how="anti")
|
|
316
|
+
.with_columns(method=pl.lit(_LIFETIME_METHOD_DEFAULT)))
|
|
317
|
+
return pl.concat([explicit, default_e], how="vertical")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Inflation factors (lazy)
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _solve_inflation_scalars(source: "InputSource"
|
|
326
|
+
) -> tuple[float, float, float]:
|
|
327
|
+
"""Read ``model.inflation_rate`` / ``model.inflation_offset_invest`` /
|
|
328
|
+
``model.inflation_offset_operations`` scalars, falling back to
|
|
329
|
+
flextool's CSV-side defaults when the Spine reader returned
|
|
330
|
+
only-default rows.
|
|
331
|
+
|
|
332
|
+
Mirrors :func:`._derived_params._solve_inflation_inputs` semantics.
|
|
333
|
+
"""
|
|
334
|
+
def _explicit_max(par: str, default: float) -> float:
|
|
335
|
+
try:
|
|
336
|
+
df = source.parameter("model", par)
|
|
337
|
+
except KeyError:
|
|
338
|
+
return default
|
|
339
|
+
if df.height == 0:
|
|
340
|
+
return default
|
|
341
|
+
try:
|
|
342
|
+
spine_default = source.parameter_default("model", par)
|
|
343
|
+
except KeyError:
|
|
344
|
+
spine_default = None
|
|
345
|
+
if spine_default is not None:
|
|
346
|
+
try:
|
|
347
|
+
spine_default_f = float(spine_default)
|
|
348
|
+
except (ValueError, TypeError):
|
|
349
|
+
spine_default_f = None
|
|
350
|
+
if spine_default_f is not None:
|
|
351
|
+
vals = df["value"].cast(pl.Float64, strict=False).to_list()
|
|
352
|
+
if vals and all(
|
|
353
|
+
v is not None and abs(v - spine_default_f) < 1e-12
|
|
354
|
+
for v in vals):
|
|
355
|
+
return default
|
|
356
|
+
try:
|
|
357
|
+
return float(df["value"].cast(pl.Float64).max())
|
|
358
|
+
except Exception:
|
|
359
|
+
return default
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
_explicit_max("inflation_rate", _INFL_RATE_DEFAULT),
|
|
363
|
+
_explicit_max("inflation_offset_investment", _INFL_OFFSET_INV_DEFAULT),
|
|
364
|
+
_explicit_max("inflation_offset_operations", _INFL_OFFSET_OPS_DEFAULT),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _years_for_period_lf(source: "InputSource",
|
|
369
|
+
active_solve: str | None,
|
|
370
|
+
period_universe: list[str],
|
|
371
|
+
) -> pl.LazyFrame:
|
|
372
|
+
"""Build the per-(d, year_label, width) frame from
|
|
373
|
+
``solve.years_represented`` mirroring
|
|
374
|
+
:func:`._derived_params._years_for_period_from_source` in lazy form.
|
|
375
|
+
|
|
376
|
+
Returns columns ``[d, y, width]`` where ``y`` is a string label.
|
|
377
|
+
Empty if no rows.
|
|
378
|
+
"""
|
|
379
|
+
# Materialise scalars eagerly — they're solve-level, tiny.
|
|
380
|
+
from ._derived_params import _years_for_period_from_source
|
|
381
|
+
yfp = _years_for_period_from_source(source, active_solve, period_universe)
|
|
382
|
+
rows: list[tuple[str, str, float]] = []
|
|
383
|
+
for d, years in yfp.items():
|
|
384
|
+
for y, w in years:
|
|
385
|
+
rows.append((d, y, w))
|
|
386
|
+
if not rows:
|
|
387
|
+
return pl.LazyFrame(schema={
|
|
388
|
+
"d": schema_dtype(_enums, "d"),
|
|
389
|
+
"y": pl.Utf8, "width": pl.Float64,
|
|
390
|
+
})
|
|
391
|
+
return pl.LazyFrame(rows, schema=["d", "y", "width"], orient="row").with_columns(
|
|
392
|
+
alias_to_axis("d", "d"))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _inflation_factors_lf(source: "InputSource",
|
|
396
|
+
active_solve: str | None,
|
|
397
|
+
period_universe: list[str],
|
|
398
|
+
) -> pl.LazyFrame:
|
|
399
|
+
"""Lazy ``(d, inv_factor, ops_factor)`` frame.
|
|
400
|
+
|
|
401
|
+
Algorithm — port of ``preprocessing/period_calculated_params.py:230-322``:
|
|
402
|
+
|
|
403
|
+
1. Scalar inputs: ``rate``, ``offset_invest``, ``offset_operations``.
|
|
404
|
+
2. For each (d, y) ∈ ``years_for_period``:
|
|
405
|
+
``base[d, y] = Σ_{y' ∈ global, y' < y} pyr[d, y']`` (default 1)
|
|
406
|
+
``until_inv[d, y] = base + pyr × offset_invest``
|
|
407
|
+
``until_ops[d, y] = base + pyr × offset_operations``
|
|
408
|
+
3. ``inv_factor[d] = Σ_y pyr[d, y] × (1+rate)^(-until_inv[d, y])``
|
|
409
|
+
``ops_factor[d] = Σ_y pyr[d, y] × (1+rate)^(-until_ops[d, y])``.
|
|
410
|
+
When ``Σ_y pyr[d, y] == 0`` → 1.0.
|
|
411
|
+
|
|
412
|
+
The cumulative-base computation (step 2) is per-period over the
|
|
413
|
+
GLOBAL year set, so it's shaped naturally as a polars window:
|
|
414
|
+
|
|
415
|
+
base[d, y] = Σ_{y'<y in global order} pyr[d, y']
|
|
416
|
+
= (cumulative_sum of pyr[d, y'] over y'<y, partitioned by d)
|
|
417
|
+
|
|
418
|
+
where unbound (d, y) pairs contribute 1 (default ``years_represented``).
|
|
419
|
+
"""
|
|
420
|
+
if not period_universe:
|
|
421
|
+
return pl.LazyFrame(schema={
|
|
422
|
+
"d": schema_dtype(_enums, "d"),
|
|
423
|
+
"inv_factor": pl.Float64, "ops_factor": pl.Float64,
|
|
424
|
+
})
|
|
425
|
+
rate, off_inv, off_ops = _solve_inflation_scalars(source)
|
|
426
|
+
one_plus_inv = (1.0 / (1.0 + rate)) if rate != -1.0 else 1.0
|
|
427
|
+
yfp_lf = _years_for_period_lf(source, active_solve, period_universe)
|
|
428
|
+
|
|
429
|
+
# Materialise per-period × per-y. At LP scale this is small (typically
|
|
430
|
+
# ≤ 100 (d, y) rows even for 50-year horizon × 20 representative years),
|
|
431
|
+
# so collecting once is cheap; the rest stays lazy.
|
|
432
|
+
yfp_eager = yfp_lf.collect()
|
|
433
|
+
period_lf = pl.LazyFrame({"d": period_universe}).with_columns(
|
|
434
|
+
alias_to_axis("d", "d"))
|
|
435
|
+
|
|
436
|
+
if yfp_eager.height == 0:
|
|
437
|
+
# No years_represented data — every period gets the trivial 1.0
|
|
438
|
+
# factor (mirrors period_calculated_params.py:300).
|
|
439
|
+
return (period_lf
|
|
440
|
+
.with_columns(
|
|
441
|
+
inv_factor=pl.lit(1.0, dtype=pl.Float64),
|
|
442
|
+
ops_factor=pl.lit(1.0, dtype=pl.Float64),
|
|
443
|
+
))
|
|
444
|
+
|
|
445
|
+
# Build the global year set with numerical sort when possible.
|
|
446
|
+
try:
|
|
447
|
+
global_years = sorted(
|
|
448
|
+
yfp_eager["y"].unique().to_list(), key=lambda y: float(y)
|
|
449
|
+
)
|
|
450
|
+
except ValueError:
|
|
451
|
+
global_years = sorted(yfp_eager["y"].unique().to_list())
|
|
452
|
+
|
|
453
|
+
# Cross-join (d, y_global) so missing rows default to width=1.0
|
|
454
|
+
# (mirrors flextool's default of p_years_represented).
|
|
455
|
+
global_y_lf = pl.LazyFrame({"y": global_years}).with_columns(
|
|
456
|
+
y_idx=pl.int_range(0, pl.len(), dtype=pl.Int64)
|
|
457
|
+
)
|
|
458
|
+
full = (period_lf
|
|
459
|
+
.join(global_y_lf, how="cross")
|
|
460
|
+
.join(yfp_eager.lazy(), on=["d", "y"], how="left")
|
|
461
|
+
.with_columns(
|
|
462
|
+
pyr=pl.col("width").fill_null(1.0),
|
|
463
|
+
))
|
|
464
|
+
|
|
465
|
+
# Cumulative base[d, y] = Σ pyr[d, y'] over y' < y (within d).
|
|
466
|
+
full = (full
|
|
467
|
+
.sort(["d", "y_idx"])
|
|
468
|
+
.with_columns(
|
|
469
|
+
base=pl.col("pyr").cum_sum().over("d") - pl.col("pyr"),
|
|
470
|
+
))
|
|
471
|
+
|
|
472
|
+
# Restrict to (d, y) actually bound to this period.
|
|
473
|
+
bound = (yfp_eager.lazy()
|
|
474
|
+
.select("d", "y")
|
|
475
|
+
.join(full, on=["d", "y"], how="inner"))
|
|
476
|
+
|
|
477
|
+
# Per-(d, y) factor contributions.
|
|
478
|
+
bound = bound.with_columns(
|
|
479
|
+
until_inv=pl.col("base") + pl.col("pyr") * off_inv,
|
|
480
|
+
until_ops=pl.col("base") + pl.col("pyr") * off_ops,
|
|
481
|
+
).with_columns(
|
|
482
|
+
inv_contrib=pl.col("pyr") * (one_plus_inv ** pl.col("until_inv")),
|
|
483
|
+
ops_contrib=pl.col("pyr") * (one_plus_inv ** pl.col("until_ops")),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
factor_per_d = (bound
|
|
487
|
+
.group_by("d")
|
|
488
|
+
.agg(
|
|
489
|
+
pl.col("inv_contrib").sum().alias("inv_factor"),
|
|
490
|
+
pl.col("ops_contrib").sum().alias("ops_factor"),
|
|
491
|
+
pl.col("pyr").sum().alias("sum_pyr"),
|
|
492
|
+
))
|
|
493
|
+
|
|
494
|
+
# Sum-pyr == 0 → factor = 1.0 (period_calculated_params.py:299-301).
|
|
495
|
+
factor_per_d = factor_per_d.with_columns(
|
|
496
|
+
inv_factor=pl.when(pl.col("sum_pyr") > 0)
|
|
497
|
+
.then(pl.col("inv_factor"))
|
|
498
|
+
.otherwise(1.0),
|
|
499
|
+
ops_factor=pl.when(pl.col("sum_pyr") > 0)
|
|
500
|
+
.then(pl.col("ops_factor"))
|
|
501
|
+
.otherwise(1.0),
|
|
502
|
+
).select("d", "inv_factor", "ops_factor")
|
|
503
|
+
|
|
504
|
+
# Periods absent from years_represented (or with R=0) get 1.0
|
|
505
|
+
# (mirrors flextool's writer default of 1.0).
|
|
506
|
+
return (period_lf
|
|
507
|
+
.join(factor_per_d, on="d", how="left")
|
|
508
|
+
.with_columns(
|
|
509
|
+
inv_factor=pl.col("inv_factor").fill_null(1.0),
|
|
510
|
+
ops_factor=pl.col("ops_factor").fill_null(1.0),
|
|
511
|
+
))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
# period_walk_iterator — the shared NPV walk
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def period_walk_iterator(
|
|
520
|
+
source: "InputSource",
|
|
521
|
+
active_solve: str | None,
|
|
522
|
+
ed_lf: pl.LazyFrame,
|
|
523
|
+
period_in_use: list[str],
|
|
524
|
+
period_universe: list[str],
|
|
525
|
+
*,
|
|
526
|
+
bounded: bool,
|
|
527
|
+
life_lf: pl.LazyFrame,
|
|
528
|
+
factor_side: str,
|
|
529
|
+
) -> pl.LazyFrame:
|
|
530
|
+
"""Δ.5-era public alias delegating to :mod:`._derived_walks`.
|
|
531
|
+
|
|
532
|
+
Δ.6 lifted the canonical implementation into ``_derived_walks`` so
|
|
533
|
+
Cluster B's invest-history walk could re-use it. This wrapper
|
|
534
|
+
preserves the boolean ``bounded`` signature for the existing NPV
|
|
535
|
+
callers while internally dispatching the new
|
|
536
|
+
:class:`._derived_walks.WindowMethod` enum.
|
|
537
|
+
|
|
538
|
+
See :func:`._derived_walks.period_walk_iterator` for the canonical
|
|
539
|
+
semantics.
|
|
540
|
+
"""
|
|
541
|
+
from ._derived_walks import (
|
|
542
|
+
period_walk_iterator as _walk,
|
|
543
|
+
WindowMethod,
|
|
544
|
+
)
|
|
545
|
+
method = WindowMethod.BOUNDED if bounded else WindowMethod.UNBOUNDED_FORWARD
|
|
546
|
+
return _walk(
|
|
547
|
+
source, active_solve, ed_lf,
|
|
548
|
+
period_in_use, period_universe,
|
|
549
|
+
window_method=method, life_lf=life_lf,
|
|
550
|
+
factor_side=factor_side)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
# Helper: per-(e, d) lifetime
|
|
555
|
+
# ---------------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _ed_lifetime_lf(source: "InputSource",
|
|
559
|
+
all_entities_lf: pl.LazyFrame,
|
|
560
|
+
periods_lf: pl.LazyFrame,
|
|
561
|
+
) -> pl.LazyFrame:
|
|
562
|
+
"""Lazy ``(e, d, life)`` mirroring ``edEntity_lifetime`` semantics.
|
|
563
|
+
|
|
564
|
+
Mirrors :func:`._derived_params._ed_lifetime_mapping`: for each
|
|
565
|
+
(e, d) ∈ entities × periods, emit ``unit/node/connection.lifetime``
|
|
566
|
+
cascaded via ``_resolve_pdX``. Non-process / non-node entities
|
|
567
|
+
get 0 (mirrors flextool's class-gated branch).
|
|
568
|
+
"""
|
|
569
|
+
ed_lf = all_entities_lf.join(periods_lf, how="cross")
|
|
570
|
+
cls_lf = _entity_class_lookup_lf(_entity_class_lf(source))
|
|
571
|
+
# Restrict to processes + nodes — other classes get life=0.
|
|
572
|
+
ed_lf = ed_lf.join(cls_lf, on="e", how="left")
|
|
573
|
+
in_class = pl.col("is_process").fill_null(False) | pl.col("is_node").fill_null(False)
|
|
574
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
575
|
+
resolved = _resolve_per_period_lf(life_per, ed_lf, fill=0.0)
|
|
576
|
+
return (resolved
|
|
577
|
+
.with_columns(
|
|
578
|
+
life=pl.when(in_class).then(pl.col("value")).otherwise(0.0),
|
|
579
|
+
)
|
|
580
|
+
.select("e", "d", "life"))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
# Per-method annuity (lazy)
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _per_method_annuity_lf(
|
|
589
|
+
source: "InputSource",
|
|
590
|
+
ed_lf: pl.LazyFrame,
|
|
591
|
+
cost_param_name: str,
|
|
592
|
+
disallowed_methods: frozenset[str],
|
|
593
|
+
) -> pl.LazyFrame:
|
|
594
|
+
"""Lazy ``(e, d, ann)`` frame computing the per-method annuity sum.
|
|
595
|
+
|
|
596
|
+
Algorithm (port of
|
|
597
|
+
``entity_annual_calc_params.py:177-217``):
|
|
598
|
+
|
|
599
|
+
For each (e, d) ∈ ed_lf:
|
|
600
|
+
ann[e, d] = Σ_{m ∈ invest_methods[e] \\ disallowed}
|
|
601
|
+
_annuity(cost[e, d], discount_rate[e, d], lifetime[e, d])
|
|
602
|
+
(when e ∈ unit ∪ connection ∪ node)
|
|
603
|
+
|
|
604
|
+
The inner computation does NOT depend on ``m`` — every allowed
|
|
605
|
+
method contributes the same ``_annuity(...)`` value. So we collapse:
|
|
606
|
+
``ann[e, d] = method_count[e] × annuity[e, d]`` where
|
|
607
|
+
``method_count[e] = |{m : (e, m) ∈ entity__invest_method,
|
|
608
|
+
m ∉ disallowed}|``.
|
|
609
|
+
"""
|
|
610
|
+
# Per-entity allowed-method count.
|
|
611
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
612
|
+
method_count_lf = (methods_lf
|
|
613
|
+
.filter(~pl.col("method").is_in(list(disallowed_methods)))
|
|
614
|
+
.group_by("e")
|
|
615
|
+
.agg(pl.col("method").count().alias("method_count")))
|
|
616
|
+
|
|
617
|
+
# Class membership gate: e ∈ unit ∪ connection ∪ node (mirrors the
|
|
618
|
+
# if/elif blocks in entity_annual_calc_params._per_method_annuity_*).
|
|
619
|
+
cls_lf = _entity_class_lookup_lf(_entity_class_lf(source))
|
|
620
|
+
in_class = (pl.col("is_process").fill_null(False)
|
|
621
|
+
| pl.col("is_node").fill_null(False))
|
|
622
|
+
|
|
623
|
+
# Per-(e, d) cost / discount_rate / lifetime via _resolve_pdX.
|
|
624
|
+
cost_per = _per_entity_param_lf(source, cost_param_name)
|
|
625
|
+
disc_per = _per_entity_param_lf(source, "discount_rate")
|
|
626
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
627
|
+
|
|
628
|
+
ed_with_class = ed_lf.join(cls_lf, on="e", how="left")
|
|
629
|
+
ed_with_cost = (_resolve_per_period_lf(cost_per, ed_with_class, fill=0.0)
|
|
630
|
+
.rename({"value": "cost"}))
|
|
631
|
+
ed_with_disc = (_resolve_per_period_lf(disc_per, ed_with_cost, fill=0.0)
|
|
632
|
+
.rename({"value": "disc"}))
|
|
633
|
+
ed_with_life = (_resolve_per_period_lf(life_per, ed_with_disc, fill=0.0)
|
|
634
|
+
.rename({"value": "life"}))
|
|
635
|
+
|
|
636
|
+
# Annuity formula: invest_value × 1000 × r / (1 - (1/(1+r))^n)
|
|
637
|
+
# with r ≤ 0 → 0.05, n ≤ 0 → 20.
|
|
638
|
+
r_eff = (pl.when(pl.col("disc") > 0).then(pl.col("disc")).otherwise(0.05))
|
|
639
|
+
n_eff = (pl.when(pl.col("life") > 0).then(pl.col("life")).otherwise(20.0))
|
|
640
|
+
annuity_expr = (pl.when(r_eff == 0)
|
|
641
|
+
.then(0.0)
|
|
642
|
+
.otherwise(
|
|
643
|
+
pl.col("cost") * 1000.0 * r_eff
|
|
644
|
+
/ (1.0 - (1.0 / (1.0 + r_eff)) ** n_eff)))
|
|
645
|
+
out = (ed_with_life
|
|
646
|
+
.join(method_count_lf, on="e", how="left")
|
|
647
|
+
.with_columns(
|
|
648
|
+
method_count=pl.col("method_count").fill_null(0),
|
|
649
|
+
ann_per_method=annuity_expr,
|
|
650
|
+
)
|
|
651
|
+
.with_columns(
|
|
652
|
+
ann=pl.when(in_class)
|
|
653
|
+
.then(pl.col("ann_per_method")
|
|
654
|
+
* pl.col("method_count").cast(pl.Float64))
|
|
655
|
+
.otherwise(0.0),
|
|
656
|
+
)
|
|
657
|
+
.select("e", "d", "ann"))
|
|
658
|
+
return out
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# ---------------------------------------------------------------------------
|
|
662
|
+
# 4 NPV variant helpers (each producing one lazy frame).
|
|
663
|
+
# ---------------------------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _entity_invest_set_lf(source: "InputSource",
|
|
667
|
+
disallowed_methods: frozenset[str],
|
|
668
|
+
) -> pl.LazyFrame:
|
|
669
|
+
"""Lazy ``[e]`` for the canonical entityInvest / entityDivest set.
|
|
670
|
+
|
|
671
|
+
Mirrors flextool's
|
|
672
|
+
``preprocessing/invest_method_sets.py::write_invest_method_sets``:
|
|
673
|
+
|
|
674
|
+
entityInvest = {e : ∃m s.t. (e, m) ∈ entity__invest_method
|
|
675
|
+
AND m ∉ invest_method_not_allowed}
|
|
676
|
+
|
|
677
|
+
``disallowed_methods`` selects between the invest variant
|
|
678
|
+
(:data:`_INVEST_NOT_ALLOWED`) and the divest variant
|
|
679
|
+
(:data:`_DIVEST_NOT_ALLOWED`). An entity with no explicit
|
|
680
|
+
``invest_method`` row is **excluded** (default is ``not_allowed``).
|
|
681
|
+
"""
|
|
682
|
+
methods_lf = _entity_method_lf(source, "invest_method")
|
|
683
|
+
return (methods_lf
|
|
684
|
+
.filter(~pl.col("method").is_in(list(disallowed_methods)))
|
|
685
|
+
.select("e")
|
|
686
|
+
.unique())
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def npv_invest_discounted_lf(
|
|
690
|
+
source: "InputSource",
|
|
691
|
+
active_solve: str | None,
|
|
692
|
+
period_invest: list[str],
|
|
693
|
+
period_in_use: list[str],
|
|
694
|
+
period_universe: list[str],
|
|
695
|
+
) -> pl.LazyFrame:
|
|
696
|
+
"""Variant 1: ``ed_entity_annual_discounted`` (invest-side NPV).
|
|
697
|
+
|
|
698
|
+
Per (e ∈ entityInvest, d ∈ period_invest)::
|
|
699
|
+
|
|
700
|
+
ann[e, d] × Σ_{d_all ∈ period_in_use, window(d, d_all)} inv_factor[d_all]
|
|
701
|
+
|
|
702
|
+
where ``window`` switches between bounded / unbounded based on
|
|
703
|
+
``e``'s lifetime_method (``reinvest_choice`` / ``no_investment`` →
|
|
704
|
+
bounded; ``reinvest_automatic`` → unbounded). An entity may have
|
|
705
|
+
multiple methods; each contributes a separate sum. See
|
|
706
|
+
``entity_annual_calc_params.py:222-251``.
|
|
707
|
+
|
|
708
|
+
Returns lazy ``[e, d, value]``.
|
|
709
|
+
"""
|
|
710
|
+
if not period_invest:
|
|
711
|
+
return pl.LazyFrame(schema={
|
|
712
|
+
"e": schema_dtype(_enums, "e"),
|
|
713
|
+
"d": schema_dtype(_enums, "d"),
|
|
714
|
+
"value": pl.Float64,
|
|
715
|
+
})
|
|
716
|
+
# entityInvest = projection of entity__invest_method via the
|
|
717
|
+
# method-allowed gate (mirror of preprocessing/invest_method_sets).
|
|
718
|
+
entity_invest_set = _entity_invest_set_lf(source, _INVEST_NOT_ALLOWED)
|
|
719
|
+
pi_lf = pl.LazyFrame({"d": period_invest}).with_columns(
|
|
720
|
+
alias_to_axis("d", "d"))
|
|
721
|
+
ed_anchor_lf = entity_invest_set.join(pi_lf, how="cross")
|
|
722
|
+
|
|
723
|
+
# Annuity (invest cost).
|
|
724
|
+
ann_lf = _per_method_annuity_lf(
|
|
725
|
+
source, ed_anchor_lf, "invest_cost", _INVEST_NOT_ALLOWED)
|
|
726
|
+
|
|
727
|
+
# Lifetime methods: per-entity dispatch.
|
|
728
|
+
all_e_lf = ed_anchor_lf.select("e").unique()
|
|
729
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
730
|
+
# Choice/no_investment → bounded; automatic → unbounded. An entity
|
|
731
|
+
# may have multiple methods; each contributes additively (mirrors
|
|
732
|
+
# the if/if structure at L237/244).
|
|
733
|
+
has_choice_or_no = (elm_lf
|
|
734
|
+
.filter(pl.col("method").is_in(
|
|
735
|
+
["reinvest_choice", "no_investment"]))
|
|
736
|
+
.select("e").unique()
|
|
737
|
+
.with_columns(has_choice=pl.lit(True)))
|
|
738
|
+
has_automatic = (elm_lf
|
|
739
|
+
.filter(pl.col("method") == "reinvest_automatic")
|
|
740
|
+
.select("e").unique()
|
|
741
|
+
.with_columns(has_automatic=pl.lit(True)))
|
|
742
|
+
|
|
743
|
+
# life_lf for the bounded walk (uses edEntity_lifetime per L236).
|
|
744
|
+
pwh_lf = pl.LazyFrame({"d": list({d for d in period_invest})}).with_columns(
|
|
745
|
+
alias_to_axis("d", "d"))
|
|
746
|
+
life_lf = _ed_lifetime_lf(source, all_e_lf, pwh_lf)
|
|
747
|
+
|
|
748
|
+
bounded = period_walk_iterator(
|
|
749
|
+
source, active_solve, ed_anchor_lf,
|
|
750
|
+
period_in_use, period_universe,
|
|
751
|
+
bounded=True, life_lf=life_lf, factor_side="inv")
|
|
752
|
+
unbounded = period_walk_iterator(
|
|
753
|
+
source, active_solve, ed_anchor_lf,
|
|
754
|
+
period_in_use, period_universe,
|
|
755
|
+
bounded=False, life_lf=life_lf, factor_side="inv")
|
|
756
|
+
|
|
757
|
+
# Combine.
|
|
758
|
+
bounded = bounded.rename({"factor": "factor_b"})
|
|
759
|
+
unbounded = unbounded.rename({"factor": "factor_u"})
|
|
760
|
+
combined = (ed_anchor_lf
|
|
761
|
+
.join(ann_lf, on=["e", "d"], how="left")
|
|
762
|
+
.join(bounded, on=["e", "d"], how="left")
|
|
763
|
+
.join(unbounded, on=["e", "d"], how="left")
|
|
764
|
+
.join(has_choice_or_no, on="e", how="left")
|
|
765
|
+
.join(has_automatic, on="e", how="left")
|
|
766
|
+
.with_columns(
|
|
767
|
+
ann=pl.col("ann").fill_null(0.0),
|
|
768
|
+
factor_b=pl.col("factor_b").fill_null(0.0),
|
|
769
|
+
factor_u=pl.col("factor_u").fill_null(0.0),
|
|
770
|
+
has_choice=pl.col("has_choice").fill_null(False),
|
|
771
|
+
has_automatic=pl.col("has_automatic").fill_null(False),
|
|
772
|
+
)
|
|
773
|
+
.with_columns(
|
|
774
|
+
value=(
|
|
775
|
+
pl.when(pl.col("has_choice"))
|
|
776
|
+
.then(pl.col("ann") * pl.col("factor_b"))
|
|
777
|
+
.otherwise(0.0)
|
|
778
|
+
+ pl.when(pl.col("has_automatic"))
|
|
779
|
+
.then(pl.col("ann") * pl.col("factor_u"))
|
|
780
|
+
.otherwise(0.0)
|
|
781
|
+
),
|
|
782
|
+
)
|
|
783
|
+
.select("e", "d", "value"))
|
|
784
|
+
return combined
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def npv_divest_discounted_lf(
|
|
788
|
+
source: "InputSource",
|
|
789
|
+
active_solve: str | None,
|
|
790
|
+
period_invest: list[str],
|
|
791
|
+
period_in_use: list[str],
|
|
792
|
+
period_universe: list[str],
|
|
793
|
+
) -> pl.LazyFrame:
|
|
794
|
+
"""Variant 2: ``ed_entity_annual_divest_discounted`` (divest-side NPV).
|
|
795
|
+
|
|
796
|
+
Per (e ∈ entityDivest, d ∈ period_invest)::
|
|
797
|
+
|
|
798
|
+
ann[e, d] × Σ_{d_all : pdy ∈ [pdy_d, pdy_d + life)} inv_factor[d_all]
|
|
799
|
+
|
|
800
|
+
where ``life`` is the **raw** ``lifetime`` value (not edEntity_lifetime),
|
|
801
|
+
and the gate is restricted to e ∈ node ∪ process (mirrors
|
|
802
|
+
L266-285 — no fallback for unclassified entities).
|
|
803
|
+
|
|
804
|
+
Returns lazy ``[e, d, value]``.
|
|
805
|
+
"""
|
|
806
|
+
if not period_invest:
|
|
807
|
+
return pl.LazyFrame(schema={
|
|
808
|
+
"e": schema_dtype(_enums, "e"),
|
|
809
|
+
"d": schema_dtype(_enums, "d"),
|
|
810
|
+
"value": pl.Float64,
|
|
811
|
+
})
|
|
812
|
+
entity_divest_set = _entity_invest_set_lf(source, _DIVEST_NOT_ALLOWED)
|
|
813
|
+
pi_lf = pl.LazyFrame({"d": period_invest}).with_columns(
|
|
814
|
+
alias_to_axis("d", "d"))
|
|
815
|
+
ed_anchor_lf = entity_divest_set.join(pi_lf, how="cross")
|
|
816
|
+
|
|
817
|
+
ann_lf = _per_method_annuity_lf(
|
|
818
|
+
source, ed_anchor_lf, "salvage_value", _DIVEST_NOT_ALLOWED)
|
|
819
|
+
|
|
820
|
+
# Use raw lifetime (per L271-284), not edEntity_lifetime. Resolve
|
|
821
|
+
# via _resolve_pdX with class gate (only nodes/processes).
|
|
822
|
+
cls_lf = _entity_class_lookup_lf(_entity_class_lf(source))
|
|
823
|
+
in_class = (pl.col("is_process").fill_null(False)
|
|
824
|
+
| pl.col("is_node").fill_null(False))
|
|
825
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
826
|
+
ed_with_class = ed_anchor_lf.join(cls_lf, on="e", how="left")
|
|
827
|
+
life_lf = (_resolve_per_period_lf(life_per, ed_with_class, fill=0.0)
|
|
828
|
+
.with_columns(
|
|
829
|
+
life=pl.when(in_class).then(pl.col("value")).otherwise(0.0),
|
|
830
|
+
)
|
|
831
|
+
.select("e", "d", "life"))
|
|
832
|
+
|
|
833
|
+
walk = period_walk_iterator(
|
|
834
|
+
source, active_solve, ed_anchor_lf,
|
|
835
|
+
period_in_use, period_universe,
|
|
836
|
+
bounded=True, life_lf=life_lf, factor_side="inv")
|
|
837
|
+
|
|
838
|
+
return (ed_anchor_lf
|
|
839
|
+
.join(ann_lf, on=["e", "d"], how="left")
|
|
840
|
+
.join(walk, on=["e", "d"], how="left")
|
|
841
|
+
.with_columns(
|
|
842
|
+
ann=pl.col("ann").fill_null(0.0),
|
|
843
|
+
factor=pl.col("factor").fill_null(0.0),
|
|
844
|
+
value=pl.col("ann") * pl.col("factor"),
|
|
845
|
+
)
|
|
846
|
+
.select("e", "d", "value"))
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _ed_fixed_cost_raw_lf(source: "InputSource",
|
|
850
|
+
ed_lf: pl.LazyFrame,
|
|
851
|
+
) -> pl.LazyFrame:
|
|
852
|
+
"""Lazy ``(e, d, fc)`` — per-entity per-period fixed cost × 1000.
|
|
853
|
+
|
|
854
|
+
Mirrors ``preprocessing/entity_period_calc_params.py:149-156``::
|
|
855
|
+
|
|
856
|
+
ed_fixed_cost[e, d] = (1000 if e ∈ node else 0) × pdNode[e, fixed_cost, d]
|
|
857
|
+
+ (1000 if e ∈ process else 0) × pdProcess[e, fixed_cost, d]
|
|
858
|
+
|
|
859
|
+
Since unit/connection/node are disjoint, the formula simplifies to:
|
|
860
|
+
fc = 1000 × resolve_pdX(fixed_cost, e, d) when e ∈ node ∪ process,
|
|
861
|
+
else 0.
|
|
862
|
+
"""
|
|
863
|
+
cls_lf = _entity_class_lookup_lf(_entity_class_lf(source))
|
|
864
|
+
in_class = (pl.col("is_process").fill_null(False)
|
|
865
|
+
| pl.col("is_node").fill_null(False))
|
|
866
|
+
fc_per = _per_entity_param_lf(source, "fixed_cost")
|
|
867
|
+
ed_with_class = ed_lf.join(cls_lf, on="e", how="left")
|
|
868
|
+
return (_resolve_per_period_lf(fc_per, ed_with_class, fill=0.0)
|
|
869
|
+
.with_columns(
|
|
870
|
+
fc=pl.when(in_class)
|
|
871
|
+
.then(pl.col("value") * 1000.0)
|
|
872
|
+
.otherwise(0.0),
|
|
873
|
+
)
|
|
874
|
+
.select("e", "d", "fc"))
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def lifetime_fixed_cost_invest_lf(
|
|
878
|
+
source: "InputSource",
|
|
879
|
+
active_solve: str | None,
|
|
880
|
+
period_with_history: list[str],
|
|
881
|
+
period_in_use: list[str],
|
|
882
|
+
period_universe: list[str],
|
|
883
|
+
) -> pl.LazyFrame:
|
|
884
|
+
"""Variant 3: ``ed_lifetime_fixed_cost`` (invest-side lifetime FC).
|
|
885
|
+
|
|
886
|
+
Per (e ∈ entity, d ∈ period_with_history)::
|
|
887
|
+
|
|
888
|
+
fc[e, d] × Σ_{d_all ∈ period_in_use, window(d, d_all)} ops_factor[d_all]
|
|
889
|
+
|
|
890
|
+
Same window dispatch as :func:`npv_invest_discounted_lf`
|
|
891
|
+
(choice/no_invest → bounded; automatic → unbounded), with
|
|
892
|
+
ops_factor (not inv_factor) and edEntity_lifetime as the bound.
|
|
893
|
+
See L292-321.
|
|
894
|
+
"""
|
|
895
|
+
if not period_with_history:
|
|
896
|
+
return pl.LazyFrame(schema={
|
|
897
|
+
"e": schema_dtype(_enums, "e"),
|
|
898
|
+
"d": schema_dtype(_enums, "d"),
|
|
899
|
+
"value": pl.Float64,
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
# Build (e, d) over all_entities × period_with_history.
|
|
903
|
+
all_e_lf = _all_entities_lf(source)
|
|
904
|
+
pwh_lf = pl.LazyFrame({"d": period_with_history}).with_columns(
|
|
905
|
+
alias_to_axis("d", "d"))
|
|
906
|
+
ed_anchor_lf = all_e_lf.join(pwh_lf, how="cross")
|
|
907
|
+
|
|
908
|
+
fc_lf = _ed_fixed_cost_raw_lf(source, ed_anchor_lf)
|
|
909
|
+
|
|
910
|
+
elm_lf = _lifetime_method_with_default_lf(source, all_e_lf)
|
|
911
|
+
has_choice_or_no = (elm_lf
|
|
912
|
+
.filter(pl.col("method").is_in(
|
|
913
|
+
["reinvest_choice", "no_investment"]))
|
|
914
|
+
.select("e").unique()
|
|
915
|
+
.with_columns(has_choice=pl.lit(True)))
|
|
916
|
+
has_automatic = (elm_lf
|
|
917
|
+
.filter(pl.col("method") == "reinvest_automatic")
|
|
918
|
+
.select("e").unique()
|
|
919
|
+
.with_columns(has_automatic=pl.lit(True)))
|
|
920
|
+
|
|
921
|
+
# life_lf via edEntity_lifetime (per L306).
|
|
922
|
+
life_lf = _ed_lifetime_lf(source, all_e_lf, pwh_lf)
|
|
923
|
+
|
|
924
|
+
bounded = period_walk_iterator(
|
|
925
|
+
source, active_solve, ed_anchor_lf,
|
|
926
|
+
period_in_use, period_universe,
|
|
927
|
+
bounded=True, life_lf=life_lf, factor_side="ops")
|
|
928
|
+
unbounded = period_walk_iterator(
|
|
929
|
+
source, active_solve, ed_anchor_lf,
|
|
930
|
+
period_in_use, period_universe,
|
|
931
|
+
bounded=False, life_lf=life_lf, factor_side="ops")
|
|
932
|
+
|
|
933
|
+
bounded = bounded.rename({"factor": "factor_b"})
|
|
934
|
+
unbounded = unbounded.rename({"factor": "factor_u"})
|
|
935
|
+
|
|
936
|
+
return (ed_anchor_lf
|
|
937
|
+
.join(fc_lf, on=["e", "d"], how="left")
|
|
938
|
+
.join(bounded, on=["e", "d"], how="left")
|
|
939
|
+
.join(unbounded, on=["e", "d"], how="left")
|
|
940
|
+
.join(has_choice_or_no, on="e", how="left")
|
|
941
|
+
.join(has_automatic, on="e", how="left")
|
|
942
|
+
.with_columns(
|
|
943
|
+
fc=pl.col("fc").fill_null(0.0),
|
|
944
|
+
factor_b=pl.col("factor_b").fill_null(0.0),
|
|
945
|
+
factor_u=pl.col("factor_u").fill_null(0.0),
|
|
946
|
+
has_choice=pl.col("has_choice").fill_null(False),
|
|
947
|
+
has_automatic=pl.col("has_automatic").fill_null(False),
|
|
948
|
+
)
|
|
949
|
+
.with_columns(
|
|
950
|
+
value=(
|
|
951
|
+
pl.when(pl.col("has_choice"))
|
|
952
|
+
.then(pl.col("fc") * pl.col("factor_b"))
|
|
953
|
+
.otherwise(0.0)
|
|
954
|
+
+ pl.when(pl.col("has_automatic"))
|
|
955
|
+
.then(pl.col("fc") * pl.col("factor_u"))
|
|
956
|
+
.otherwise(0.0)
|
|
957
|
+
),
|
|
958
|
+
)
|
|
959
|
+
.select("e", "d", "value"))
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def lifetime_fixed_cost_divest_lf(
|
|
963
|
+
source: "InputSource",
|
|
964
|
+
active_solve: str | None,
|
|
965
|
+
period_invest: list[str],
|
|
966
|
+
period_in_use: list[str],
|
|
967
|
+
period_universe: list[str],
|
|
968
|
+
) -> pl.LazyFrame:
|
|
969
|
+
"""Variant 4: ``ed_lifetime_fixed_cost_divest`` (divest-side FC).
|
|
970
|
+
|
|
971
|
+
Per (e ∈ entityDivest, d ∈ period_invest)::
|
|
972
|
+
|
|
973
|
+
fc[e, d] × Σ_{d_all : pdy ∈ [pdy_d, pdy_d + life)} inv_factor[d_all]
|
|
974
|
+
|
|
975
|
+
where ``life`` is raw lifetime and ``inv_factor`` (NOT ops_factor)
|
|
976
|
+
is used — mod L1651 asymmetry. See L325-348.
|
|
977
|
+
"""
|
|
978
|
+
if not period_invest:
|
|
979
|
+
return pl.LazyFrame(schema={
|
|
980
|
+
"e": schema_dtype(_enums, "e"),
|
|
981
|
+
"d": schema_dtype(_enums, "d"),
|
|
982
|
+
"value": pl.Float64,
|
|
983
|
+
})
|
|
984
|
+
entity_divest_set = _entity_invest_set_lf(source, _DIVEST_NOT_ALLOWED)
|
|
985
|
+
pi_lf = pl.LazyFrame({"d": period_invest}).with_columns(
|
|
986
|
+
alias_to_axis("d", "d"))
|
|
987
|
+
ed_anchor_lf = entity_divest_set.join(pi_lf, how="cross")
|
|
988
|
+
|
|
989
|
+
fc_lf = _ed_fixed_cost_raw_lf(source, ed_anchor_lf)
|
|
990
|
+
|
|
991
|
+
# Raw lifetime (gated on class).
|
|
992
|
+
cls_lf = _entity_class_lookup_lf(_entity_class_lf(source))
|
|
993
|
+
in_class = (pl.col("is_process").fill_null(False)
|
|
994
|
+
| pl.col("is_node").fill_null(False))
|
|
995
|
+
life_per = _per_entity_param_lf(source, "lifetime")
|
|
996
|
+
ed_with_class = ed_anchor_lf.join(cls_lf, on="e", how="left")
|
|
997
|
+
life_lf = (_resolve_per_period_lf(life_per, ed_with_class, fill=0.0)
|
|
998
|
+
.with_columns(
|
|
999
|
+
life=pl.when(in_class).then(pl.col("value")).otherwise(0.0),
|
|
1000
|
+
)
|
|
1001
|
+
.select("e", "d", "life"))
|
|
1002
|
+
|
|
1003
|
+
walk = period_walk_iterator(
|
|
1004
|
+
source, active_solve, ed_anchor_lf,
|
|
1005
|
+
period_in_use, period_universe,
|
|
1006
|
+
bounded=True, life_lf=life_lf, factor_side="inv")
|
|
1007
|
+
|
|
1008
|
+
return (ed_anchor_lf
|
|
1009
|
+
.join(fc_lf, on=["e", "d"], how="left")
|
|
1010
|
+
.join(walk, on=["e", "d"], how="left")
|
|
1011
|
+
.with_columns(
|
|
1012
|
+
fc=pl.col("fc").fill_null(0.0),
|
|
1013
|
+
factor=pl.col("factor").fill_null(0.0),
|
|
1014
|
+
value=pl.col("fc") * pl.col("factor"),
|
|
1015
|
+
)
|
|
1016
|
+
.select("e", "d", "value"))
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
# ---------------------------------------------------------------------------
|
|
1020
|
+
# Per-entity-class helpers (lazy)
|
|
1021
|
+
# ---------------------------------------------------------------------------
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _all_entities_lf(source: "InputSource") -> pl.LazyFrame:
|
|
1025
|
+
"""Lazy ``[e]`` frame: union of unit + node + connection."""
|
|
1026
|
+
return _entity_class_lf(source).select("e").unique()
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
# ---------------------------------------------------------------------------
|
|
1030
|
+
# Public Param-producing entry points
|
|
1031
|
+
# ---------------------------------------------------------------------------
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def p_inflation_op_from_source(source: "InputSource",
|
|
1035
|
+
active_solve: str | None,
|
|
1036
|
+
period_in_use: list[str],
|
|
1037
|
+
period_universe: list[str] | None = None,
|
|
1038
|
+
) -> "Param | None":
|
|
1039
|
+
"""Public entry: ``p_inflation_op[d]`` over ``period_in_use``.
|
|
1040
|
+
|
|
1041
|
+
Lazy port of
|
|
1042
|
+
:func:`._derived_params.p_inflation_op_full_cascade_from_source`.
|
|
1043
|
+
Replaces the workdir read of ``solve_data/p_inflation_factor_operations_yearly.csv``
|
|
1044
|
+
when wired through :func:`apply_npv`.
|
|
1045
|
+
"""
|
|
1046
|
+
if not period_in_use:
|
|
1047
|
+
return None
|
|
1048
|
+
if period_universe is None:
|
|
1049
|
+
period_universe = period_in_use
|
|
1050
|
+
factors_lf = _inflation_factors_lf(source, active_solve, period_universe)
|
|
1051
|
+
out = (pl.LazyFrame({"d": period_in_use})
|
|
1052
|
+
.with_columns(alias_to_axis("d", "d"))
|
|
1053
|
+
.join(factors_lf, on="d", how="left")
|
|
1054
|
+
.with_columns(
|
|
1055
|
+
value=pl.col("ops_factor").fill_null(1.0),
|
|
1056
|
+
)
|
|
1057
|
+
.select("d", "value")
|
|
1058
|
+
.sort("d")
|
|
1059
|
+
.collect())
|
|
1060
|
+
if out.height == 0:
|
|
1061
|
+
return None
|
|
1062
|
+
return Param(("d",), out)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def p_ed_fixed_cost_from_source(source: "InputSource",
|
|
1066
|
+
period_with_history: list[str],
|
|
1067
|
+
) -> "Param | None":
|
|
1068
|
+
"""Public entry: ``p_ed_fixed_cost[e, d]`` over ``period_with_history``.
|
|
1069
|
+
|
|
1070
|
+
Replaces the workdir read of ``solve_data/ed_fixed_cost.csv``.
|
|
1071
|
+
Drops zero-valued rows (mirrors the loader's ``filter(value != 0)``).
|
|
1072
|
+
"""
|
|
1073
|
+
if not period_with_history:
|
|
1074
|
+
return None
|
|
1075
|
+
all_e_lf = _all_entities_lf(source)
|
|
1076
|
+
pwh_lf = pl.LazyFrame({"d": period_with_history}).with_columns(
|
|
1077
|
+
alias_to_axis("d", "d"))
|
|
1078
|
+
ed_lf = all_e_lf.join(pwh_lf, how="cross")
|
|
1079
|
+
fc_lf = _ed_fixed_cost_raw_lf(source, ed_lf).rename({"fc": "value"})
|
|
1080
|
+
out = (fc_lf
|
|
1081
|
+
.filter(pl.col("value") != 0.0)
|
|
1082
|
+
.select("e", "d", "value")
|
|
1083
|
+
.sort("e", "d")
|
|
1084
|
+
.collect())
|
|
1085
|
+
if out.height == 0:
|
|
1086
|
+
return None
|
|
1087
|
+
return Param(("e", "d"), out)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def ed_entity_annual_discounted_from_source(
|
|
1091
|
+
source: "InputSource",
|
|
1092
|
+
active_solve: str | None,
|
|
1093
|
+
period_invest: list[str],
|
|
1094
|
+
period_in_use: list[str],
|
|
1095
|
+
period_universe: list[str],
|
|
1096
|
+
) -> "Param | None":
|
|
1097
|
+
"""Public entry: ``ed_entity_annual_discounted[e, d]``.
|
|
1098
|
+
|
|
1099
|
+
Returns the full ``entityInvest × period_invest`` frame, including
|
|
1100
|
+
zero-valued rows, to mirror the snapshot CSV produced by
|
|
1101
|
+
:func:`._emit_entity_annual.write_entity_annual_calc_params`
|
|
1102
|
+
(which the loader's ``_read_e_d`` seed retains unfiltered). The
|
|
1103
|
+
loader's ``apply_npv`` only overwrites the seed when this entry
|
|
1104
|
+
returns a non-None frame, so emitting the zero rows is required
|
|
1105
|
+
for the lazy result to match the seed on fixtures where every
|
|
1106
|
+
(e, d) value is zero (e.g. ``work_wind_battery_invest``).
|
|
1107
|
+
"""
|
|
1108
|
+
if not period_invest:
|
|
1109
|
+
return None
|
|
1110
|
+
out = (npv_invest_discounted_lf(
|
|
1111
|
+
source, active_solve,
|
|
1112
|
+
period_invest, period_in_use, period_universe)
|
|
1113
|
+
.select("e", "d", "value")
|
|
1114
|
+
.sort("e", "d")
|
|
1115
|
+
.collect())
|
|
1116
|
+
if out.height == 0:
|
|
1117
|
+
return None
|
|
1118
|
+
return Param(("e", "d"), out)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def ed_entity_annual_divest_discounted_from_source(
|
|
1122
|
+
source: "InputSource",
|
|
1123
|
+
active_solve: str | None,
|
|
1124
|
+
period_invest: list[str],
|
|
1125
|
+
period_in_use: list[str],
|
|
1126
|
+
period_universe: list[str],
|
|
1127
|
+
) -> "Param | None":
|
|
1128
|
+
"""Public entry: ``ed_entity_annual_divest_discounted[e, d]``.
|
|
1129
|
+
|
|
1130
|
+
Unfiltered ``entityDivest × period_invest`` — see the rationale on
|
|
1131
|
+
:func:`ed_entity_annual_discounted_from_source`.
|
|
1132
|
+
"""
|
|
1133
|
+
if not period_invest:
|
|
1134
|
+
return None
|
|
1135
|
+
out = (npv_divest_discounted_lf(
|
|
1136
|
+
source, active_solve,
|
|
1137
|
+
period_invest, period_in_use, period_universe)
|
|
1138
|
+
.select("e", "d", "value")
|
|
1139
|
+
.sort("e", "d")
|
|
1140
|
+
.collect())
|
|
1141
|
+
if out.height == 0:
|
|
1142
|
+
return None
|
|
1143
|
+
return Param(("e", "d"), out)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def ed_lifetime_fixed_cost_from_source(
|
|
1147
|
+
source: "InputSource",
|
|
1148
|
+
active_solve: str | None,
|
|
1149
|
+
period_with_history: list[str],
|
|
1150
|
+
period_in_use: list[str],
|
|
1151
|
+
period_universe: list[str],
|
|
1152
|
+
) -> "Param | None":
|
|
1153
|
+
"""Public entry: ``ed_lifetime_fixed_cost[e, d]``.
|
|
1154
|
+
|
|
1155
|
+
Unfiltered ``entity × period_with_history`` — see the rationale on
|
|
1156
|
+
:func:`ed_entity_annual_discounted_from_source`.
|
|
1157
|
+
"""
|
|
1158
|
+
if not period_with_history:
|
|
1159
|
+
return None
|
|
1160
|
+
out = (lifetime_fixed_cost_invest_lf(
|
|
1161
|
+
source, active_solve,
|
|
1162
|
+
period_with_history, period_in_use, period_universe)
|
|
1163
|
+
.select("e", "d", "value")
|
|
1164
|
+
.sort("e", "d")
|
|
1165
|
+
.collect())
|
|
1166
|
+
if out.height == 0:
|
|
1167
|
+
return None
|
|
1168
|
+
return Param(("e", "d"), out)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def ed_lifetime_fixed_cost_divest_from_source(
|
|
1172
|
+
source: "InputSource",
|
|
1173
|
+
active_solve: str | None,
|
|
1174
|
+
period_invest: list[str],
|
|
1175
|
+
period_in_use: list[str],
|
|
1176
|
+
period_universe: list[str],
|
|
1177
|
+
) -> "Param | None":
|
|
1178
|
+
"""Public entry: ``ed_lifetime_fixed_cost_divest[e, d]``.
|
|
1179
|
+
|
|
1180
|
+
Unfiltered ``entityDivest × period_invest`` — see the rationale on
|
|
1181
|
+
:func:`ed_entity_annual_discounted_from_source`.
|
|
1182
|
+
"""
|
|
1183
|
+
if not period_invest:
|
|
1184
|
+
return None
|
|
1185
|
+
out = (lifetime_fixed_cost_divest_lf(
|
|
1186
|
+
source, active_solve,
|
|
1187
|
+
period_invest, period_in_use, period_universe)
|
|
1188
|
+
.select("e", "d", "value")
|
|
1189
|
+
.sort("e", "d")
|
|
1190
|
+
.collect())
|
|
1191
|
+
if out.height == 0:
|
|
1192
|
+
return None
|
|
1193
|
+
return Param(("e", "d"), out)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
# ---------------------------------------------------------------------------
|
|
1197
|
+
# Apply boundary
|
|
1198
|
+
# ---------------------------------------------------------------------------
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def apply_npv(flex_data: object,
|
|
1202
|
+
source: "InputSource",
|
|
1203
|
+
workdir: Path,
|
|
1204
|
+
*,
|
|
1205
|
+
provider: "object | None" = None) -> None:
|
|
1206
|
+
"""Wire the cluster-A NPV helpers into ``flex_data`` (mutates in place).
|
|
1207
|
+
|
|
1208
|
+
Order:
|
|
1209
|
+
1. ``p_inflation_op`` (depends on years_represented)
|
|
1210
|
+
2. ``p_ed_fixed_cost`` (depends on period_with_history)
|
|
1211
|
+
3. ``ed_entity_annual_discounted`` (depends on inv_factor + ed_invest)
|
|
1212
|
+
4. ``ed_entity_annual_divest_discounted``
|
|
1213
|
+
(depends on inv_factor + ed_divest)
|
|
1214
|
+
5. ``ed_lifetime_fixed_cost`` (depends on ops_factor + period_with_history)
|
|
1215
|
+
6. ``ed_lifetime_fixed_cost_divest`` (depends on inv_factor + ed_divest)
|
|
1216
|
+
|
|
1217
|
+
Δ.12b — assignment is unconditional (no silent fall-through to a
|
|
1218
|
+
seed value). Each helper raises on hard errors and returns
|
|
1219
|
+
``None`` only when the upstream feature is genuinely inactive
|
|
1220
|
+
(no_invest short-circuit, no period_with_history, etc.) — that
|
|
1221
|
+
``None`` is the explicit "field stays default" signal.
|
|
1222
|
+
"""
|
|
1223
|
+
from ._derived_params import (
|
|
1224
|
+
_read_active_solve, _solve_periods, _period_in_use_set,
|
|
1225
|
+
_periodAll_from_source, _read_period_with_history,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
active_solve = _read_active_solve(workdir, provider=provider)
|
|
1229
|
+
period_in_use = _period_in_use_set(source, active_solve, workdir, provider=provider)
|
|
1230
|
+
period_universe = _periodAll_from_source(source, active_solve, workdir=workdir, provider=provider)
|
|
1231
|
+
period_invest = _solve_periods(source, active_solve, "invest_periods") or []
|
|
1232
|
+
period_with_history = (_read_period_with_history(workdir, provider=provider)
|
|
1233
|
+
or list(period_in_use))
|
|
1234
|
+
|
|
1235
|
+
# 1. p_inflation_op (None == no inflation data → field stays default).
|
|
1236
|
+
flex_data.p_inflation_op = p_inflation_op_from_source(
|
|
1237
|
+
source, active_solve, period_in_use, period_universe)
|
|
1238
|
+
|
|
1239
|
+
# 2. p_ed_fixed_cost — None == no period_with_history (single-solve
|
|
1240
|
+
# fixtures with no historical periods); legitimate "no data" outcome.
|
|
1241
|
+
flex_data.p_ed_fixed_cost = p_ed_fixed_cost_from_source(
|
|
1242
|
+
source, period_with_history)
|
|
1243
|
+
|
|
1244
|
+
# 3-6. NPV / lifetime cascade family. Mirrors the "no_invest"
|
|
1245
|
+
# short-circuit in :mod:`._derived_params.ed_entity_annual_family_from_source`
|
|
1246
|
+
# and :func:`flextool.engine_polars.input._load_invest`: when
|
|
1247
|
+
# neither ed_invest nor ed_divest carries any row, the cluster
|
|
1248
|
+
# outputs are all None (the LP has no v_invest / v_divest
|
|
1249
|
+
# variables that would consume them).
|
|
1250
|
+
ed_invest = getattr(flex_data, "ed_invest_set", None)
|
|
1251
|
+
ed_divest = getattr(flex_data, "ed_divest_set", None)
|
|
1252
|
+
no_invest = (
|
|
1253
|
+
(ed_invest is None or ed_invest.height == 0)
|
|
1254
|
+
and (ed_divest is None or ed_divest.height == 0)
|
|
1255
|
+
)
|
|
1256
|
+
if no_invest or active_solve is None:
|
|
1257
|
+
return
|
|
1258
|
+
|
|
1259
|
+
# Δ.18 — when the override returns None (e.g. synthetic per-sub-solve
|
|
1260
|
+
# ``active_solve`` whose ``invest_periods`` is empty in Spine), preserve
|
|
1261
|
+
# the seed-loaded snapshot CSV value rather than overwriting it. The
|
|
1262
|
+
# override remains authoritative when it returns a non-None Param.
|
|
1263
|
+
def _set_if(field: str, value):
|
|
1264
|
+
if value is not None:
|
|
1265
|
+
setattr(flex_data, field, value)
|
|
1266
|
+
|
|
1267
|
+
_set_if("ed_entity_annual_discounted",
|
|
1268
|
+
ed_entity_annual_discounted_from_source(
|
|
1269
|
+
source, active_solve,
|
|
1270
|
+
period_invest, period_in_use, period_universe))
|
|
1271
|
+
|
|
1272
|
+
_set_if("ed_entity_annual_divest_discounted",
|
|
1273
|
+
ed_entity_annual_divest_discounted_from_source(
|
|
1274
|
+
source, active_solve,
|
|
1275
|
+
period_invest, period_in_use, period_universe))
|
|
1276
|
+
|
|
1277
|
+
_set_if("ed_lifetime_fixed_cost",
|
|
1278
|
+
ed_lifetime_fixed_cost_from_source(
|
|
1279
|
+
source, active_solve,
|
|
1280
|
+
period_with_history, period_in_use, period_universe))
|
|
1281
|
+
|
|
1282
|
+
_set_if("ed_lifetime_fixed_cost_divest",
|
|
1283
|
+
ed_lifetime_fixed_cost_divest_from_source(
|
|
1284
|
+
source, active_solve,
|
|
1285
|
+
period_invest, period_in_use, period_universe))
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
__all__ = [
|
|
1289
|
+
"period_walk_iterator",
|
|
1290
|
+
"p_inflation_op_from_source",
|
|
1291
|
+
"p_ed_fixed_cost_from_source",
|
|
1292
|
+
"ed_entity_annual_discounted_from_source",
|
|
1293
|
+
"ed_entity_annual_divest_discounted_from_source",
|
|
1294
|
+
"ed_lifetime_fixed_cost_from_source",
|
|
1295
|
+
"ed_lifetime_fixed_cost_divest_from_source",
|
|
1296
|
+
"apply_npv",
|
|
1297
|
+
]
|