22
33import logging
44from datetime import datetime
5+ from enum import Enum , auto
56from pathlib import Path
67from typing import TYPE_CHECKING
78
1112from genno import Computer
1213from iam_units import registry
1314
15+ from message_ix_models .util .genno import append
16+
1417from .key import gdp_cap , pdt_nyt
1518from .key import report as k_report
1619
2023 # NB The following is itself within an if TYPE_CHECKING block; mypy can't find it
2124 from plotnine .typing import PlotAddable # type: ignore [attr-defined]
2225
23- from .config import Config
2426
2527log = logging .getLogger (__name__ )
2628
3032logging .getLogger ("matplotlib.font_manager" ).setLevel (logging .INFO + 1 )
3133
3234
35+ class Kind (Enum ):
36+ """Kinds of transport :class:`Plots <.Plot>`."""
37+
38+ #: Plots for :func:`.transport.build.get_computer`, that is, during the process of
39+ #: building a single scenario. These are used within a :class:`~.genno.Computer`,
40+ #: without a built or solved Scenario.
41+ BUILD = auto ()
42+
43+ #: Plots for pre-solve or build-process analysis of multiple scenarios. These are
44+ #: added by :func:`.transport.build.debug_multi`.
45+ BUILD_MULTI = auto ()
46+
47+ #: Plots for post-solve reporting of single MESSAGEix-Transport scenarios. These are
48+ #: added to a :class:`.Reporter` via :func:`.transport.report.callback`.
49+ REPORT = auto ()
50+
51+ #: Plots of post-solve reporting of multiple MESSAGEix-Transport scenarios, or
52+ #: of data reported from these. These are added by :class:`.transport.report.multi`.
53+ REPORT_MULTI = auto ()
54+
55+
3356class LabelFirst :
3457 """Labeller that labels the first item using a format string.
3558
@@ -54,6 +77,9 @@ class Plot(genno.compat.plotnine.Plot):
5477 This class extends :class:`genno.compat.plotnine.Plot` with extra features.
5578 """
5679
80+ #: Kind of plot.
81+ kind : Kind = Kind .REPORT
82+
5783 #: 'Static' geoms: list of plotnine objects that are not dynamic
5884 static : list ["PlotAddable" ] = [
5985 p9 .theme (figure_size = (11.7 , 8.3 )),
@@ -66,9 +92,6 @@ class Plot(genno.compat.plotnine.Plot):
6692 #: Units expression for plot title.
6793 unit : str | None = None
6894
69- #: :obj:`False` for plots not intended to be run on a solved scenario.
70- runs_on_solved_scenario : bool = True
71-
7295 def ggtitle (self , extra : str | None = None ):
7396 """Return :class:`plotnine.ggtitle` including the current date & time."""
7497 title_parts = [
@@ -119,7 +142,7 @@ def add_tasks(
119142 # Output path for this parameter
120143 k_path = f"plot { cls .basename } path"
121144 filename = f"{ cls .basename } { cls .suffix } "
122- if cls .runs_on_solved_scenario :
145+ if cls .kind is Kind . REPORT :
123146 # Make a path including the Scenario URL
124147 c .add (k_path , "make_output_path" , "config" , name = filename )
125148 else :
@@ -212,7 +235,7 @@ class ComparePDT(Plot):
212235
213236 This plot is used in :func:`.transport.build.debug_multi`, not in ordinary
214237 reporting. Rather than receiving data from computed quantities already in the graph,
215- it reads them from files named :file:`pdt.csv` (per :attr:`.kind `) in the
238+ it reads them from files named :file:`pdt.csv` (per :attr:`.measure `) in the
216239 directories generated by the workflow steps like "SSP1 debug build" (
217240 :func:`.transport.build.debug`).
218241
@@ -221,7 +244,8 @@ class ComparePDT(Plot):
221244 - One line with points per scenario, coloured by scenario.
222245 """
223246
224- runs_on_solved_scenario = False
247+ kind = Kind .BUILD_MULTI
248+
225249 basename = "compare-pdt"
226250
227251 static = Plot .static + [
@@ -234,15 +258,15 @@ class ComparePDT(Plot):
234258 ]
235259
236260 #: Base name for source data files, for instance :file:`pdt.csv`.
237- kind = "pdt"
261+ measure = "pdt"
238262
239263 #: Units of input files
240264 unit = "km/a"
241265 #: Unit adjustment factor.
242266 factor = 1e6
243267
244268 def generate (self , * paths : Path ):
245- data = read_csvs (self .kind , * paths ).eval ("value = value / @self.factor" )
269+ data = read_csvs (self .measure , * paths ).eval ("value = value / @self.factor" )
246270
247271 # Add factor to the unit expression
248272 if self .factor != 1.0 :
@@ -259,7 +283,7 @@ class ComparePDTCap0(ComparePDT):
259283 """
260284
261285 basename = "compare-pdt-cap"
262- kind = "pdt-cap"
286+ measure = "pdt-cap"
263287 factor = 1e3
264288
265289
@@ -284,7 +308,7 @@ class ComparePDTCap1(Plot):
284308 - One line with points per |t| (=transport mode), coloured by mode.
285309 """
286310
287- runs_on_solved_scenario = False
311+ kind = Kind . BUILD_MULTI
288312 basename = "compare-pdt-capita-gdp"
289313 static = PDT_CAP_GDP_STATIC + [
290314 p9 .facet_wrap ("scenario" , ncol = 5 ),
@@ -526,7 +550,7 @@ class DemandCap(Plot):
526550
527551 def generate (self , data , commodities , cg ):
528552 # Convert and select data
529- data = data .query (f"c in { repr ( list (map (str , commodities ))) } " ).pipe (c_group , cg )
553+ data = data .query (f"c in { list (map (str , commodities ))!r } " ).pipe (c_group , cg )
530554 for _ , ggplot in self .groupby_plot (data , "n" ):
531555 yield ggplot
532556
@@ -544,7 +568,7 @@ def _reduce_units(df: pd.DataFrame, target_units) -> tuple[pd.DataFrame, str]:
544568class DemandExo (Plot ):
545569 """Passenger transport activity."""
546570
547- runs_on_solved_scenario = False
571+ kind = Kind . BUILD
548572 basename = "demand-exo"
549573 inputs = [pdt_nyt ]
550574 static = Plot .static + [
@@ -566,7 +590,7 @@ def generate(self, data):
566590class DemandExoCap0 (Plot ):
567591 """Passenger transport activity per person."""
568592
569- runs_on_solved_scenario = False
593+ kind = Kind . BUILD
570594 basename = "demand-exo-capita"
571595 inputs = [pdt_nyt + "capita+post" ]
572596 static = Plot .static + [
@@ -591,7 +615,7 @@ class DemandExoCap1(DemandExoCap0):
591615 Unlike :class:`DemandExoCap0`, this uses GDP per capita as the abscissa/x-aesthetic.
592616 """
593617
594- runs_on_solved_scenario = False
618+ kind = Kind . BUILD
595619 basename = "demand-exo-capita-gdp"
596620 inputs = [pdt_nyt + "capita+post" , gdp_cap ]
597621 static = PDT_CAP_GDP_STATIC
@@ -663,10 +687,10 @@ def generate(self, y0: int, data):
663687
664688
665689class MultiStock (Plot ):
666- """Stock, LDV technologies, SSP × policy ."""
690+ """LDV technology stock ."""
667691
692+ kind = Kind .REPORT_MULTI
668693 basename = "multi-stock"
669- runs_on_solved_scenario = False
670694
671695 static = Plot .static + [
672696 p9 .aes (x = "y" , y = "value" , color = "v" ),
@@ -692,7 +716,7 @@ class Scale1Diff(Plot):
692716
693717 basename = "scale-1-diff"
694718
695- runs_on_solved_scenario = False
719+ kind = Kind . BUILD
696720 inputs = ["scale-1:nl-t-c-l-h:a" , "scale-1:nl-t-c-l-h:b" ]
697721
698722 _s , _v , _y = "scenario" , "value" , "t + ' ' + c"
@@ -776,40 +800,36 @@ def generate(self, data):
776800 yield ggplot + p9 .expand_limits (y = [0 , y_max ])
777801
778802
779- def prepare_computer (c : Computer ):
803+ def prepare_computer (
804+ c : Computer , * , kind : Kind = Kind .REPORT , target : "KeyLike" = k_report .all
805+ ) -> None :
780806 """Add :data:`.PLOTS` to `c`.
781807
782808 Adds:
783809
784810 - 1 key like **"plot inv-cost"** corresponding to the :attr:`~.Plot.basename` of
785- each :class:`.Plot` subclass defined in this module.
811+ each :class:`.Plot` subclass defined in this module with a matching `kind` .
786812 - The key **"transport plots"** that triggers writing all the plots to file.
787813 """
814+ # Force matplotlib to use a non-interactive backend for plotting
788815 import matplotlib
789816
790- # Force matplotlib to use a non-interactive backend for plotting
791817 matplotlib .use ("pdf" )
792818
793- keys = []
794-
795- config : "Config" = c .graph ["config" ]["transport" ]
796-
797819 # Iterate over the Plot subclasses defined in the current module
820+ keys = []
798821 for plot in filter (
799- lambda cls : isinstance (cls , type ) and issubclass (cls , Plot ) and cls is not Plot ,
822+ lambda cls : isinstance (cls , type )
823+ and issubclass (cls , Plot )
824+ and cls is not Plot
825+ and cls .kind is kind ,
800826 globals ().values (),
801827 ):
802- if (not plot .runs_on_solved_scenario and config .with_solution ) or (
803- False # Use True here or uncomment below to skip some or all plots
804- # "stock" not in plot.basename
805- ):
806- log .info (f"Skip { plot } " )
807- continue
808828 keys .append (f"plot { plot .basename } " )
809829 c .add (keys [- 1 ], plot )
810830
811- key = "transport plots"
812- log .info (f"Add { repr (key )} collecting { len (keys )} plots" )
813- c .add (key , keys )
831+ log .info (f"Add { k_report .plot !r} collecting { len (keys )} plots" )
832+ c .add (k_report .plot , "summarize" , keys )
814833
815- c .graph [k_report .all ].append (key )
834+ # Extend the task at `target` with k_report.plot
835+ append (c , target , k_report .plot )
0 commit comments