API Reference

Simulators

Markov chain simulator using 2D kernel density estimation (KDE).

Simulates a continuous Markov-chain process by fitting a 2D KDE to the joint probability distribution of (previous, current) value pairs and sampling from the resulting conditional distribution p(current | previous).

Sampling is confined to the physical support declared by a VariableSpec, so out-of-support values are unsamplable by construction rather than merely unlikely. See docs/design/variable-specs-and-initialisation.md section 3.

class meteosynth.markov_chain_simulator_kde2d.Kind(value)[source]

Bases: Enum

What kind of transition the data supports – not what kind of variable it is.

Degeneracy is empirical: which hours are constant depends on latitude and season, so it is detected from the data and cannot be declared in a spec. Irradiance is identically zero at night and so degenerates there; wind and temperature never do.

Replaces the old two-letter singularity_type codes:

Kind

meaning

was

KDE_2D

previous and current both vary

NN

KDE_1D

previous constant, current varies

CN

CONSTANT

current constant

NC, CC

NC and CC collapse into one: both already dispatched to the same routine (return the constant), and the distinction never affected behaviour.

CONSTANT = 3
KDE_1D = 2
KDE_2D = 1
class meteosynth.markov_chain_simulator_kde2d.MarkovChainSimulator2dKDE(data: DataFrame, bandwidth: float | None = None, spec: VariableSpec | None = None)[source]

Bases: object

A class to simulate continuous Markov chain processes using a 2D KDE for joint probability distribution between previous and current values.

Parameters:
  • data (pd.DataFrame) – DataFrame containing previous and current columns of floats.

  • bandwidth (float, optional) – Bandwidth for the KDE. None uses Scott’s rule.

  • spec (VariableSpec, optional) – Physical constraints of the quantity. When given, the evaluation domain is clamped to spec.support in both sampling paths, making out-of-support samples impossible. When None the domain is the observed range alone, which is the old (unconstrained) behaviour.

calculate_next_state_cdf(bandwidth: float = None, update_bandwidth: bool = True) DataFrame[source]

_summary_

Parameters:

bandwidth (float, optional) – _description_. Defaults to None.

Returns:

_description_

Return type:

pd.DataFrame

current_values: array
generate_sequence(start_state: float, length: int, rng: None | int | Generator = None) List[float][source]

Generate a Markov sequence of states starting from start_state.

Parameters:
  • start_state (float) – The initial state of the sequence.

  • length (int) – Number of transitions to simulate. The returned list has length + 1 elements (the start state plus each transition).

  • rng (None, int or np.random.Generator, optional) – Source of randomness, resolved once so the whole trajectory is reproducible while remaining stochastic from step to step.

Returns:

The generated sequence, including start_state as the first item.

Return type:

List[float]

get_conditional_density(prev_value: float, resolution: int = 1000) Tuple[ndarray, ndarray][source]

Get the conditional probability density function for a given previous value.

Parameters:

x_valuesnp.ndarray

Array of the x domain values to evaluate the conditional density

prev_valuefloat

The previous value to condition on

resolutionint

Number of points to evaluate the conditional density

Returns:

Tuple containing:
  • points: Array of current values

  • density: Conditional probability density at each point

get_conditional_density_gen(prev_value: float, x_values: array = None) Tuple[ndarray, ndarray][source]

Generate the conditional probability density function given a previous value.

This method creates a conditional probability density function p(current|previous) by evaluating the joint density of the previous and current values, then normalizing.

Parameters:
  • prev_value (float) – The previous value to condition on.

  • x_values (np.array, optional) – Array of x values at which to evaluate the conditional density. If None, a default grid will be created based on the domain.

Returns:

A tuple containing:

  • Array of x values

  • Corresponding conditional density values

Return type:

Tuple[np.ndarray, np.ndarray]

Notes

The conditional density is calculated as p(current|previous) = p(previous, current) / p(previous). If the joint KDE is not available (typically when there is no variation in the data), a fallback singularity joint density will be used.

get_next_state(current_state: float, rng: None | int | Generator = None) float[source]

Sample the next state, dispatching on kind.

Parameters:
  • current_state (float) – The current state from which to transition. Ignored when kind is not Kind.KDE_2D – a 1D or constant model has nothing to condition on.

  • rng (None, int or np.random.Generator, optional) – Source of randomness. An int is treated as a seed for a fresh generator; None draws from a fresh unseeded one. Pass a Generator to draw successive values from one stream, which is what the chain walkers do.

Returns:

The next state, guaranteed inside self.spec.support when a spec is set.

Return type:

float

property is_degenerate: bool

True when the data does not support a full 2D conditional model.

kind: Kind | None = None

What the data supports. See Kind.

plot_conditional_densities(prev_values: List[float], resolution: int = 1000) Tuple[Figure, Axes][source]

Plot conditional probability densities for multiple previous values.

Parameters:

prev_valuesList[float]

List of previous values to show conditional distributions for

resolutionint

Number of points to evaluate each conditional density

Returns:

fig, ax: Figure and Axes objects

plot_joint_distribution(resolution: int = 100) Tuple[Figure, Axes][source]

Plot the joint distribution of previous and current values.

Parameters:

resolutionint

Grid resolution for plotting

Returns:

fig, ax: Figure and Axes objects

plot_transition_slice(prev_value: float, resolution: int = 100) Tuple[Figure, Axes][source]

Visualize a slice through the joint distribution at a specific previous value.

Parameters:

prev_valuefloat

The previous value to slice at

resolutionint

Grid resolution for plotting

Returns:

fig, ax: Figure and Axes objects

previous_values: array
summary() DataFrame[source]

Return a summary DataFrame of the model.

Returns:

pd.DataFrame with statistics about the data and model

class meteosynth.markov_implementations.markov_chain_simulator_continuous.ContinuousMarkovChainSimulator(data: DataFrame, bandwidth: float | None = None)[source]

Bases: object

A class to simulate Markov chain processes with continuous (float) state values using kernel density estimation for transition modeling.

generate_sequence(start_state: float, length: int, seed: int | None = None) List[float][source]

Generate a sequence of states starting from the given state.

Parameters:

start_statefloat

The starting state for the sequence

lengthint

The number of transitions to simulate (resulting sequence will be length+1)

seedint, optional

Random seed for reproducibility

Returns:

List of states including the start state and subsequent transitions

get_next_state(current_state: float, seed: int | None = None) float[source]

Generate the next state based on the current state using conditional density.

Parameters:

current_statefloat

The current state from which to transition

seedint, optional

Random seed for reproducibility

Returns:

float: The next state sampled from the conditional distribution

plot_sample_transitions(from_values: List[float], n_samples: int = 1000) Tuple[Figure, Axes][source]

Plot sample transitions from multiple starting states.

Parameters:

from_valuesList[float]

List of starting states

n_samplesint

Number of samples to draw for each starting state

Returns:

fig, ax: Figure and Axes objects

plot_transition_density(from_value: float, points: int = 1000, range_padding: float = 1.0) Tuple[Figure, Axes][source]

Plot the transition probability density function for a given state.

Parameters:

from_valuefloat

The starting state

pointsint

Number of points to evaluate the density function

range_paddingfloat

Factor to extend the range of the plot beyond observed data

Returns:

fig, ax: Figure and Axes objects

summary() DataFrame[source]

Return a summary DataFrame of the transition model.

Returns:

pd.DataFrame with statistics about transitions from each state group

class meteosynth.markov_implementations.markov_chain_simulator_discrete.MarkovChainSimulatorDiscrete(data: DataFrame)[source]

Bases: object

A class to simulate Markov chain processes based on transition probabilities derived from historical data.

generate_sequence(start_state: int | float | str, length: int, seed: int | None = None) List[int | float | str][source]

Generate a sequence of states starting from the given state.

Parameters:

start_stateint, float, or str

The starting state for the sequence

lengthint

The number of transitions to simulate (resulting sequence will be length+1)

seedint, optional

Random seed for reproducibility

Returns:

List of states including the start state and subsequent transitions

get_next_state(current_state: int | float | str, seed: int | None = None) int | float | str[source]

Generate the next state based on the current state.

Parameters:

current_stateint, float, or str

The current state from which to transition

seedint, optional

Random seed for reproducibility

Returns:

The next state according to transition probabilities

get_transition_probabilities(state: int | float | str) Series[source]

Get transition probabilities from a specific state to all other states.

Parameters:

stateint, float, or str

The state for which to get transition probabilities

Returns:

pd.Series containing transition probabilities to all possible states

summary() DataFrame[source]

Return a summary DataFrame of the transition matrix.

Returns:

pd.DataFrame representation of the transition probability matrix

Daily Series Generators

Daily synthetic-series generators built on top of the Markov-chain simulators.

The main entry point is EnvSeriesGenerator, which manages an initial distribution plus 23 hourly MarkovChainSimulator2dKDE transition models, and stitches them together into a complete 24-hour profile of a meteorological attribute (e.g. plane-of-array direct irradiance poa_direct, air temperature, wind speed).

This is the piece intended for Monte-Carlo studies of energy-project output and requirements: loop EnvSeriesGenerator.gen_day() for independent synthetic days, or call DayWindowGenerator.generate_series() for a continuous chained series.

Training data can be sliced two ways, and both are supported:

class meteosynth.generators.DayWindowGenerator(mdp: MetDataProcessor, attr_str: str, n_days: int = 15, bandwidth: float | None = 0.1, cache_size: int = 8)[source]

Bases: object

Seasonal generator built on rolling day windows, fitted lazily per target day.

Wraps a MetDataProcessor and hands out an EnvSeriesGenerator for any calendar day, trained on the +/- n_days window centred on that day. Unlike a month-based split, the training data is centred on the target and varies smoothly from day to day.

Generators are built on demand and held in a small LRU cache rather than precomputed. Holding all 366 days costs ~126 MB for one attribute (~880 MB across all seven), while fitting one costs only ~58 ms – a gaussian_kde “fit” just stores its 992 bytes of points and a 2x2 covariance. Since callers walk the calendar in order, a small cache hits nearly always, and a whole synthetic year costs ~21 s of fitting while holding a few hundred KB.

Persisting fitted generators to disk is deliberately not offered: it would write numbers already present in the source data, ~31x duplicated across overlapping windows, to save 58 ms – and it does not work at the default bandwidth=0.1 anyway, since scipy’s gaussian_kde stores an unpicklable local lambda in covariance_factor. See docs/design/completed/rolling-day-window.md.

Parameters:
  • mdp (MetDataProcessor) – Processor holding the source meteorological data.

  • attr_str (str) – Attribute to model. Must have a registered VariableSpec.

  • n_days (int, optional) – Half-width of the training window in days. Defaults to 15 (a 31-day window).

  • bandwidth (float, optional) – KDE bandwidth forwarded to every hourly simulator.

  • cache_size (int, optional) – Number of fitted generators to keep in memory. Defaults to 8 (~2.8 MB).

Examples

>>> dwg = DayWindowGenerator(mdp, attr_str='poa_direct')
>>> profile = dwg.gen_day(2, 29)
>>> year = dwg.generate_series(start=(1, 1), n_days=365)
clear_cache() None[source]

Drop every cached generator, releasing their memory.

for_day(month: int, day: int) EnvSeriesGenerator[source]

Get the generator trained on the window centred on (month, day).

Feb 29 is an ordinary target and needs no special handling.

Parameters:
  • month (int) – Month of the target day (1-12).

  • day (int) – Day of the target month.

Returns:

Fitted generator, built on first request and cached thereafter.

Return type:

EnvSeriesGenerator

for_slot(slot: int) EnvSeriesGenerator[source]

Get the generator for an all_leap calendar slot (1-366).

Parameters:

slot (int) – Calendar slot, as produced by day_slot().

Returns:

Fitted generator, built on first request and cached thereafter.

Return type:

EnvSeriesGenerator

gen_day(month: int, day: int, rng=None, seed: int | None = None) ndarray[source]

Generate a single synthetic 24-hour profile for a calendar day.

Hour 0 is drawn from that day’s pi(x0). For a chained series across many days, use generate_series() instead.

Parameters:
  • month (int) – Month of the target day (1-12).

  • day (int) – Day of the target month.

  • rng (None, int or np.random.Generator, optional) – Source of randomness.

  • seed (int, optional) – Convenience alias for rng.

Returns:

Array of shape (24,) with one value per hour.

Return type:

np.ndarray

generate_series(start: tuple, n_days: int, rng=None, seed: int | None = None, include_feb29: bool = False) ndarray[source]

Generate a continuous synthetic series, chained across midnight.

Day 1 starts from its own pi(x0). Every later day’s hour 0 is drawn from that day’s cross-midnight model, conditioned on the previous day’s 23:00 value, and its hours 1-23 from that day’s within-day transitions. The chain is therefore time-inhomogeneous at daily resolution: each calendar day uses models fitted to its own +/- n_days window, so the models drift smoothly through the season.

An ensemble of independent days is n_days=1 called repeatedly: pi(x0) is redrawn at the start of every call, and the cross-midnight model is used only within a call. The boundary of the call is the reset.

Parameters:
  • start (tuple of (int, int)) – (month, day) of the first day. Required: pi(x0) is a dated object – midnight temperature in January and in July are different distributions, so a series with no start date has no well-defined initial distribution.

  • n_days (int) – Number of consecutive days to generate.

  • rng (None, int or np.random.Generator, optional) – Source of randomness, resolved once for the whole series.

  • seed (int, optional) – Convenience alias for rng.

  • include_feb29 (bool, optional) – Whether the walk visits Feb 29. Defaults to False, giving a 365-day year: the series then steps 28 Feb 23:00 -> 1 Mar 00:00 using 1 March’s cross-midnight model, which is fitted from real 1 March dates whose actual predecessor was 29 Feb in leap years and 28 Feb otherwise.

Returns:

Array of shape (n_days, 24); row i is the i-th calendar day.

Return type:

np.ndarray

Examples

>>> year = dwg.generate_series(start=(1, 1), n_days=365, seed=1)
>>> ens = np.vstack([dwg.generate_series((3, 14), 1, rng=rng)
...                  for _ in range(500)])
class meteosynth.generators.EnvSeriesGenerator(met_data: DataFrame, attr_str: str, bandwidth: float | None = 0.1, wide_full: DataFrame | None = None)[source]

Bases: object

One day’s worth of model for a meteorological attribute. Date-blind.

Holds the 25 models that describe a single day:

  • pi(x0) – the initial distribution, fitted to observed 00:00 values. Used when the day starts a chain.

  • 23 within-day transitions h-1 -> h for h = 1..23.

  • one cross-midnight transition 23:00(d-1) -> 00:00(d), used when the day continues a chain. Only built when wide_full is supplied, since it needs the full record rather than the training subset.

This class deliberately knows nothing about which day it models: the caller chose the season by choosing the subset. Trajectories therefore live on DayWindowGenerator, which does know the calendar. See docs/design/variable-specs-and-initialisation.md.

Parameters:
  • met_data (pd.DataFrame) – Processed meteorological data with hour and attr_str columns. Typically a month subset (get_month_subset()) or a rolling day window (get_day_window_subset()); both are accepted unchanged.

  • attr_str (str) – Attribute to model. Must have a registered VariableSpec.

  • bandwidth (float, optional) – KDE bandwidth forwarded to every simulator.

  • wide_full (pd.DataFrame, optional) – The full record pivoted to days-by-hours, used to build the cross-midnight model. Without it gen_day_from() is unavailable. DayWindowGenerator pivots this once and shares it across all 366 days.

property cross_midnight: MarkovChainSimulator2dKDE | None

00(d) model, or None if not built.

Type:

The fitted 23

Type:

00(d-1) -> 00

gen_day(rng=None, seed: int | None = None) ndarray[source]

Generate one synthetic day, starting the chain from pi(x0).

Parameters:
  • rng (None, int or np.random.Generator, optional) – Source of randomness. Pass a Generator to draw many days from one stream; pass an int for a self-contained reproducible day.

  • seed (int, optional) – Convenience alias for rng, when rng is not given.

Returns:

Array of shape (24,) with one value per hour.

Return type:

np.ndarray

gen_day_from(previous_2300: float, rng=None) ndarray[source]

Generate one synthetic day continuing from the previous day’s 23:00 value.

Hour 0 is drawn from this day’s cross-midnight model conditioned on previous_2300; hours 1-23 then follow the ordinary within-day transitions.

Parameters:
  • previous_2300 (float) – The 23:00 value of the preceding day.

  • rng (None, int or np.random.Generator, optional) – Source of randomness.

Returns:

Array of shape (24,).

Return type:

np.ndarray

Raises:

ValueError – If no cross-midnight model was built (no wide_full at construction, or no target day in the window had a predecessor in the record).

property initial: MarkovChainSimulator2dKDE

The fitted initial distribution pi(x0).

property simulators: dict[int, MarkovChainSimulator2dKDE]

The within-day hourly simulators, keyed by hour of day (1-23).

meteosynth.generators.FEB29_SLOT = 60

Slot of Feb 29 on the all_leap calendar. A synthetic non-leap year skips it.

meteosynth.generators.build_cross_midnight_pairs(wide_full: DataFrame, target_index: Index) DataFrame[source]

Build 23:00(d-1) -> 00:00(d) pairs from actual consecutive dates.

This is the model that carries a series across midnight, and the one place where the climatological window and the calendar must not be confused.

Parameters:
  • wide_full (pd.DataFrame) – Days-by-hours frame over the full record, indexed by YYYYMMDD strings. Must be the whole record, not the window: the previous day of a target day may legitimately fall outside the window.

  • target_index (pd.Index) – The YYYYMMDD index of the target days – typically a window subset’s days.

Returns:

previous (23:00 of the preceding calendar day) and current (00:00 of the target day), one row per target day that has a real predecessor in the record.

Return type:

pd.DataFrame

Notes

Target days whose predecessor is absent drop out – the first day of the record always does, since its predecessor precedes the record.

The trap this avoids: the training window wraps climatologically, so a Jan 2 window contains late-December days. Pairing on the calendar slot rather than the actual date would then pair Dec 31 2012 with Jan 1 2012 – a 364-day backwards jump presented as a one-hour step. Looking the predecessor up by real date makes that impossible.

meteosynth.generators.build_initial_values(wide: DataFrame) DataFrame[source]

Build the sample that the initial distribution pi(x0) is fitted to.

pi(x0) is the distribution of the first value of the chain. Unlike a transition it is unconditional – hour 0 has no previous hour, so there is nothing to condition on. It is simply the observed 00:00 value of every training day.

The frame is returned in previous/current form, with a constant previous, so it can be fitted by the ordinary MarkovChainSimulator2dKDE. That is not a trick: a constant previous is exactly the Kind.KDE_1D case, which already means “there is no information in the previous value, sample the marginal”. When the 00:00 values are themselves constant – as they are for irradiance, which is zero every midnight – it degenerates one step further to Kind.CONSTANT and returns that constant. “Irradiance starts at zero” therefore falls out of the data rather than being hard-coded.

Parameters:

wide (pd.DataFrame) – Days-by-hours frame, as produced by convert_to_daily_wide().

Returns:

Frame with constant previous and the observed 00:00 values as current.

Return type:

pd.DataFrame

meteosynth.generators.build_transition_pairs(wide: DataFrame, hour: int) DataFrame[source]

Build previous/current transition pairs for one hour from a wide frame.

wide is a days-by-hours frame as produced by convert_to_daily_wide(): one row per day, one column per hour. Both values of a pair are therefore read from the same row, which is what guarantees they come from the same day.

That guarantee is the whole point of taking the wide frame rather than two hour-filtered slices of the long frame. Slicing and zipping by position is only correct while both slices happen to share a day ordering; it fails silently – same lengths, no exception, ~27k mispaired rows – as soon as anything re-sorts the input. Here, alignment is keyed on the date index, so it holds under any ordering. See docs/design/completed/transition-pair-alignment.md.

Days with a missing value at either hour are dropped, and only those days.

Parameters:
  • wide (pd.DataFrame) – Days-by-hours frame, indexed by date, with integer hour columns.

  • hour (int) – Target hour of the day (1-23). Hour 0 is not a valid target – see Notes.

Returns:

Frame with previous and current float columns, one row per day.

Return type:

pd.DataFrame

Raises:

ValueError – If hour is outside 1-23.

Notes

Valid for hours 1-23 only. Hour 0 has no previous hour within the same day, and the chain no longer fakes one: it is started from an initial distribution (build_initial_values()) or continued from the previous calendar day (build_cross_midnight_pairs()). The old behaviour used hour_prev = (hour - 1) % 24, making hour 0’s “previous” hour 23 of the same day – a 23-hour backwards jump rather than a 1-hour step. Measured on the shipped 2011-2012 record, those same-day pairs correlate at 0.360 for wind_speed and 0.890 for temp_air, against 0.981 and 0.998 for the real cross-midnight step.

On data with no missing values – which is what PVGIS supplies – this returns pairs bit-for-bit identical to the positional construction it replaced. It deviates in one respect, deliberately: where the source has a NaN, the positional version emitted a pair containing it (poisoning the KDE), whereas this drops that day.

meteosynth.generators.generate_hr_simulator(met_data: DataFrame, hour: int, attr_str: str, bandwidth: float | None = 0.1) MarkovChainSimulator2dKDE[source]

Build a Markov-chain simulator for the transition into a specific hour.

The current hour’s values (current) are conditioned on the previous hour’s values (previous), so the resulting simulator models the hour-to-hour transition hour-1 -> hour.

Parameters:
  • met_data (pd.DataFrame) – Processed meteorological data containing at least an hour column and the requested attr_str column.

  • hour (int) – Target hour of the day (0-23) for which to build the transition model.

  • attr_str (str) – Attribute to model. Must have a registered VariableSpec.

  • bandwidth (float, optional) – KDE bandwidth passed to the simulator. Defaults to 0.1 for smooth transitions; use None for Scott’s rule.

Returns:

A fitted simulator for the hour-1 -> hour transition.

Return type:

MarkovChainSimulator2dKDE

Notes

This pivots met_data on every call. Building all 24 hourly simulators this way costs ~12x more in pivoting than in KDE fitting, so EnvSeriesGenerator pivots once and shares the result instead. This function remains as the convenient single-hour entry point.

Data Processing

The canonical calendar schema, and what ships pre-registered.

“Which columns matter” is no longer a fixed list. A column is modellable if and only if it has a registered VariableSpec, so meteosynth.variables is the authority and this module holds only the calendar index plus a documentation-grade note of what ships registered out of the box.

That inversion is deliberate. MODELLED_ATTRS used to be a whitelist that also doubled as the ingestion requirement, which is how Int – a PVGIS quality flag – became a modellable “meteorological attribute”. Making the registry authoritative means a user can model rel_humidity or snow_depth by declaring what it physically is, with no change to this package, and means provider quirk columns fall out by rule rather than by a hand-maintained exclusion list.

Pure data, no pandas – mirrors the free-function style of meteosynth.day_window.

meteosynth.constants.INDEX_COLS: tuple[str, ...] = ('year', 'month', 'day', 'hour')

Chronological index columns every record must carry.

meteosynth.constants.SHIPPED_ATTRS: tuple[str, ...] = ('poa_global', 'poa_direct', 'poa_sky_diffuse', 'poa_ground_diffuse', 'temp_air', 'wind_speed')

What ships pre-registered, for documentation and error messages. This gates nothing: meteosynth.variables.registered() is what generators validate against, and it grows when a user calls meteosynth.variables.register().

meteosynth.constants.modelled_attrs() tuple[str, ...][source]

Every currently registered column name.

Thin pass-through to meteosynth.variables.registered(), kept so callers with a “what can I model?” question have an obvious place to ask it. It is a function rather than a constant because the registry is extensible at runtime – a module level tuple would be a stale snapshot taken at import time.

class meteosynth.met_data_processor.MetDataProcessor(met_data: DataFrame)[source]

Bases: object

Slice a canonical meteorological frame by month or by rolling day window.

Accepts any frame carrying INDEX_COLS plus at least one column with a registered VariableSpec. The frame may come from meteosynth.adapters or straight from the user – adapters are an optional collection helper, not a required layer.

On ingestion the frame is validated (validate_columns()) and trimmed to the registered columns (trim_columns()).

describe_time_range()[source]

Describe the time range of the meteorological data.

Returns: str: Description of the time range.

get_day_window_subset(month: int, day: int, n_days: int = 15)[source]

Get a rolling window of the meteorological data centred on a calendar day.

Selects every record lying within n_days of (month, day) on the all_leap calendar, pooled across all years in the dataset. This is the seasonal counterpart to get_month_subset(): where a month subset is centred on the middle of the month (so a May 1 model trains on data centred on May 16) and jumps discontinuously at month boundaries, a window is centred on the target day and slides smoothly from one day to the next.

The window wraps across New Year, so a Jan 2 target reaches back into December and a Dec 30 target reaches forward into January. Feb 29 needs no special handling: it is an ordinary slot, so it is pooled into any nearby window and can also be requested as the target.

Note this is a climatological window, not a chronological one – a Jan 2 2011 target pools December 2011 as a proxy for “late-December climate”.

Parameters:
  • month (int) – Month of the target day (1-12).

  • day (int) – Day of the target month.

  • n_days (int, optional) – Half-width of the window in days. Defaults to 15, giving a 31-day window (~62 days pooled over 2 years, matching a month subset’s sample size). n_days=0 selects the target day alone; any n_days >= 183 selects the whole year.

Returns:

Subset of the meteorological data for the window, with the same date_id column that get_month_subset() adds, plus the day_slot of each record.

Return type:

pd.DataFrame

Raises:

ValueError – If (month, day) is not a valid date, or n_days is negative.

get_month_subset(month_ind: int)[source]

Get a subset of the meteorological data for a specific month.

Parameters: month_ind (int): Month index (1-12) to filter the data.

Returns: pd.DataFrame: Subset of the meteorological data for the specified month.

get_monthly_attribute_wide_format(value_col: str, month_id: int = None)[source]

Convert the meteorological data to wide format using pivot_table.

Parameters: value_col (str): The column to use as the values.

Returns: pd.DataFrame: The converted met_data DataFrame in wide format.

meteosynth.met_data_processor.convert_to_daily_wide(met_data: DataFrame, value_col: str)[source]

Convert a DataFrame to wide format using pivot_table.

Parameters: met_data (pd.DataFrame): The DataFrame to convert. value_col (str): The column to use as the values.

Returns: pd.DataFrame: The converted met_data DataFrame in wide format.

meteosynth.met_data_processor.trim_columns(met_data: DataFrame) DataFrame[source]

Return a copy holding only the index columns and registered variables.

Unregistered columns are dropped rather than carried through, so nothing downstream can come to depend on a provider-specific column – which is exactly how a PVGIS quality flag ended up being treated as a meteorological quantity.

solar_elevation and Int fall out by this rule, not by a hardcoded list: neither is a stochastic environmental quantity, so neither has a spec. A user who genuinely wants to model solar elevation registers a spec for it and it survives. Declaring it is the deliberate act.

Note the caller’s original frame is never mutated – only this processed copy is trimmed – so a dropped column remains available to the caller.

meteosynth.met_data_processor.validate_columns(met_data: DataFrame) None[source]

Check a frame carries the calendar index and at least one modellable column.

The rule is conditional – all of INDEX_COLS, and any registered column – which is why it is a function rather than the flat REQUIRED_COLS tuple it replaces: a tuple of things that must all be present cannot express “all of these and any of those”.

Requiring only one registered column means a temperature-only source with no irradiance at all ingests cleanly. “Is this frame usable for what you asked?” is a different question, answered later by the generator’s attr_str validation, which can name the specific column that is missing.

Raises:

ValueError – If an index column is missing, or no registered column is present.

Canonical calendar helpers for rolling day-of-year windows.

Transition models can be trained either on a calendar-month subset (get_month_subset()) or on a rolling window of +/- n days centred on a target day (get_day_window_subset()). This module provides the calendar arithmetic the latter needs, as pure functions over (month, day) pairs with no pandas dependency.

Days are indexed by a slot on the all_leap calendar (CF-conventions all_leap / 366_day): every year is treated as if it had 366 days, so Feb 29 owns slot 60 and Mar 1 is always slot 61 regardless of the year:

Jan 1 -> 1  ...  Feb 28 -> 59,  Feb 29 -> 60,  Mar 1 -> 61  ...  Dec 31 -> 366

Non-leap years simply never emit slot 60. This keeps the mapping leap-independent, so records pooled from leap and non-leap years line up. Note that pandas’ .dt.dayofyear does not have this property: it shifts by one after February in leap years, which would misalign the whole March-December calendar when pooling years.

See docs/design/completed/rolling-day-window.md for the full rationale.

meteosynth.day_window.DAYS_IN_YEAR = 366

Number of slots in the canonical all_leap year.

meteosynth.day_window.MAX_HALF_WIDTH = 183

Maximum possible circular distance between two slots.

meteosynth.day_window.circular_delta(slots: int | ndarray, center: int, period: int = 366) ndarray[source]

Circular distance in days between slots and center.

The calendar wraps, so the distance from Dec 31 (366) to Jan 1 (1) is 1, not 365.

Parameters:
  • slots (int or np.ndarray) – Slot(s) to measure.

  • center (int) – Slot at the centre of the window.

  • period (int, optional) – Length of the calendar. This is not a free parameter – it is fixed by the slot index and defaults to DAYS_IN_YEAR. The slot axis has unit spacing over 1..366, so only period=366 makes the Dec 31 -> Jan 1 wrap cost exactly 1 day. Passing 365 collapses Dec 31 onto Jan 1; passing 365.25 or 366.25 opens a phantom gap at the wrap. It is exposed only so that the invariant can be tested explicitly.

Returns:

Distance(s) in days, in 0..period // 2.

Return type:

np.ndarray

Examples

>>> int(circular_delta(day_slot(1, 1), day_slot(12, 31)))
1
meteosynth.day_window.day_slot(month: int, day: int) int[source]

Map a calendar (month, day) to its slot on the all_leap calendar.

The mapping is leap-independent: Feb 29 is an ordinary slot (60), and every date after February keeps the same slot in leap and non-leap years alike.

Parameters:
  • month (int) – Month of the year (1-12).

  • day (int) – Day of the month (1-31), validated against the all_leap month length, so (2, 29) is accepted but (2, 30) and (4, 31) are not.

Returns:

Slot in 1..366.

Return type:

int

Raises:

ValueError – If month is outside 1-12, or day is not a valid day of that month.

Examples

>>> day_slot(1, 1)
1
>>> day_slot(2, 29)
60
>>> day_slot(12, 31)
366
meteosynth.day_window.day_slots(months, days) ndarray[source]

Vectorised day_slot() for array-likes of months and days.

Parameters:
  • months (array-like of int) – Broadcastable arrays of months (1-12) and days of the month.

  • days (array-like of int) – Broadcastable arrays of months (1-12) and days of the month.

Returns:

Integer array of slots in 1..366, with the broadcast shape.

Return type:

np.ndarray

Raises:

ValueError – If any (month, day) pair is not a valid all_leap date.

meteosynth.day_window.slot_to_month_day(slot: int) Tuple[int, int][source]

Invert day_slot().

Parameters:

slot (int) – Slot in 1..366.

Returns:

The (month, day) owning that slot.

Return type:

tuple of (int, int)

Raises:

ValueError – If slot is outside 1..366.

Examples

>>> slot_to_month_day(60)
(2, 29)
meteosynth.day_window.window_slots(month: int, day: int, n_days: int = 15) ndarray[source]

The slots lying within n_days of (month, day), wrapping across New Year.

Parameters:
  • month (int) – Month of the target day (1-12).

  • day (int) – Day of the target month.

  • n_days (int, optional) – Half-width of the window in days. Defaults to 15, giving a 31-slot window. n_days=0 selects the target day alone; any n_days >= 183 selects the whole year.

Returns:

Sorted array of slots, of length min(2 * n_days + 1, 366).

Return type:

np.ndarray

Raises:

ValueError – If n_days is negative, or (month, day) is not a valid date.

Examples

Feb 29 is an ordinary target – no special-casing needed:

>>> len(window_slots(2, 29, n_days=15))
31

The window wraps across the year boundary:

>>> [slot_to_month_day(s) for s in window_slots(1, 2, n_days=4)]
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (12, 29), (12, 30), (12, 31)]

PVGIS data-retrieval helpers.

Thin wrappers around pvlib.iotools for fetching hourly and Typical Meteorological Year (TMY) data from the PVGIS (Photovoltaic Geographical Information System) service, plus small utilities to prepare the returned frames for the rest of meteosynth.

Importing this module has no side effects (no network calls, no file writes, no plots). Run it as a script (python -m meteosynth.metdata_pvgis) to execute the small demonstration in __main__.

meteosynth.metdata_pvgis.add_time_columns(data: DataFrame) DataFrame[source]

Add year, month, day and hour columns from a datetime index.

Parameters:

data (pd.DataFrame) – A frame indexed by a DatetimeIndex (as returned by pvlib.iotools.get_pvgis_hourly()).

Returns:

The same frame with the four calendar columns added (in place and returned for convenience).

Return type:

pd.DataFrame

meteosynth.metdata_pvgis.fetch_pvgis_hourly(latitude: float, longitude: float, start_year: int, end_year: int, usehorizon: bool = True, components: bool = True) DataFrame[source]

Fetch hourly irradiance / weather data from PVGIS for a site.

Parameters:
  • latitude (float) – Site coordinates in decimal degrees.

  • longitude (float) – Site coordinates in decimal degrees.

  • start_year (int) – Inclusive range of years to retrieve.

  • end_year (int) – Inclusive range of years to retrieve.

  • usehorizon (bool) – Whether to account for the local horizon (default True).

  • components (bool) – Whether to return separate irradiance components (default True).

Returns:

Hourly data with year, month, day and hour columns added.

Return type:

pd.DataFrame

meteosynth.metdata_pvgis.fetch_pvgis_tmy(latitude: float, longitude: float, coerce_year: int | None = None) Tuple[DataFrame, dict][source]

Fetch a Typical Meteorological Year (TMY) dataset from PVGIS.

Parameters:
  • latitude (float) – Site coordinates in decimal degrees.

  • longitude (float) – Site coordinates in decimal degrees.

  • coerce_year (int, optional) – If given, all timestamps are coerced to this year.

Returns:

The TMY weather frame and the associated site metadata.

Return type:

tuple of (pd.DataFrame, dict)

meteosynth.metdata_pvgis.save_hourly(data: DataFrame, path: Path = PosixPath('data/pvgis_hourly_data.xlsx')) Path[source]

Save an hourly frame to Excel, creating the parent directory if needed.

Plotting Utilities

meteosynth.sup_plot_lib.plot_2d_kde_sns(month_data, value_str: str = 'poa_direct', ylabel='Plane of Array Direct Irradiance (W/m^2)', figsize=(10, 6), contour_levels=5)[source]

Create a 2D KDE plot with filled contours and contour lines using seaborn.

Parameters: - month_data: DataFrame containing the data to plot. - figsize: Tuple specifying the figure size. - contour_levels: Number of contour levels to display.

meteosynth.sup_plot_lib.plot_hourly_mean_with_error_bars_plt(met_data, value_str: str = 'poa_direct', k=1.0, show_points: bool = True)[source]

Create a plot of the hourly mean and standard error for ‘poa_direct’ with error bars.

meteosynth.sup_plot_lib.plot_month_by_year_sns(mdp: MetDataProcessor, month_ind: int = 5, value_col: str = 'poa_direct')[source]

Create a faceted plot of the monthly data for each year using Seaborn.

Parameters: - mdp: MetDataProcessor instance. - month_ind: Month index (1-12) to filter the data. - value_col: Column name for the value to plot.

Returns: - FacetGrid object.

meteosynth.sup_plot_lib.plot_whisker_point_sns(month_data, value_str: str = 'poa_direct', ylabel='Plane of Array Direct Irradiance (W/m^2)')[source]

Create a boxplot with scatter points overlayed using seaborn.