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:
EnumWhat 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_typecodes:Kindmeaning
was
KDE_2Dprevious and current both vary
NNKDE_1Dprevious constant, current varies
CNCONSTANTcurrent constant
NC,CCNCandCCcollapse 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:
objectA 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
previousandcurrentcolumns of floats.bandwidth (float, optional) – Bandwidth for the KDE.
Noneuses Scott’s rule.spec (VariableSpec, optional) – Physical constraints of the quantity. When given, the evaluation domain is clamped to
spec.supportin both sampling paths, making out-of-support samples impossible. WhenNonethe 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 + 1elements (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_stateas 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
kindis notKind.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;
Nonedraws from a fresh unseeded one. Pass aGeneratorto draw successive values from one stream, which is what the chain walkers do.
- Returns:
The next state, guaranteed inside
self.spec.supportwhen a spec is set.- Return type:
float
- property is_degenerate: bool¶
True when the data does not support a full 2D conditional model.
- 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¶
- class meteosynth.markov_implementations.markov_chain_simulator_continuous.ContinuousMarkovChainSimulator(data: DataFrame, bandwidth: float | None = None)[source]¶
Bases:
objectA 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
- class meteosynth.markov_implementations.markov_chain_simulator_discrete.MarkovChainSimulatorDiscrete(data: DataFrame)[source]¶
Bases:
objectA 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
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:
by calendar month, via
get_month_subset();by a rolling window of +/- n days centred on a target day, via
get_day_window_subset()– wrapped ergonomically byDayWindowGenerator, which builds each day’s generator lazily rather than holding all 366 in memory at once.
- class meteosynth.generators.DayWindowGenerator(mdp: MetDataProcessor, attr_str: str, n_days: int = 15, bandwidth: float | None = 0.1, cache_size: int = 8)[source]¶
Bases:
objectSeasonal generator built on rolling day windows, fitted lazily per target day.
Wraps a
MetDataProcessorand hands out anEnvSeriesGeneratorfor any calendar day, trained on the +/-n_dayswindow 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.1anyway, sincescipy’sgaussian_kdestores an unpicklable local lambda incovariance_factor. Seedocs/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)
- 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:
- for_slot(slot: int) EnvSeriesGenerator[source]¶
Get the generator for an
all_leapcalendar 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:
- 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_dayswindow, so the models drift smoothly through the season.An ensemble of independent days is
n_days=1called 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); rowiis thei-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:
objectOne 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 -> hfor h = 1..23.one cross-midnight transition
23:00(d-1) -> 00:00(d), used when the day continues a chain. Only built whenwide_fullis 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. Seedocs/design/variable-specs-and-initialisation.md.- Parameters:
met_data (pd.DataFrame) – Processed meteorological data with
hourandattr_strcolumns. 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.DayWindowGeneratorpivots 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
Generatorto draw many days from one stream; pass an int for a self-contained reproducible day.seed (int, optional) – Convenience alias for
rng, whenrngis 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_fullat 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_leapcalendar. 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
YYYYMMDDstrings. 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
YYYYMMDDindex of the target days – typically a window subset’s days.
- Returns:
previous(23:00 of the preceding calendar day) andcurrent(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/currentform, with a constantprevious, so it can be fitted by the ordinaryMarkovChainSimulator2dKDE. That is not a trick: a constantpreviousis exactly theKind.KDE_1Dcase, 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 toKind.CONSTANTand 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
previousand the observed 00:00 values ascurrent.- Return type:
pd.DataFrame
- meteosynth.generators.build_transition_pairs(wide: DataFrame, hour: int) DataFrame[source]¶
Build
previous/currenttransition pairs for one hour from a wide frame.wideis a days-by-hours frame as produced byconvert_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
previousandcurrentfloat columns, one row per day.- Return type:
pd.DataFrame
- Raises:
ValueError – If
houris 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 usedhour_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 forwind_speedand 0.890 fortemp_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 transitionhour-1 -> hour.- Parameters:
met_data (pd.DataFrame) – Processed meteorological data containing at least an
hourcolumn and the requestedattr_strcolumn.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.1for smooth transitions; useNonefor Scott’s rule.
- Returns:
A fitted simulator for the
hour-1 -> hourtransition.- Return type:
Notes
This pivots
met_dataon every call. Building all 24 hourly simulators this way costs ~12x more in pivoting than in KDE fitting, soEnvSeriesGeneratorpivots 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 callsmeteosynth.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:
objectSlice a canonical meteorological frame by month or by rolling day window.
Accepts any frame carrying
INDEX_COLSplus at least one column with a registeredVariableSpec. The frame may come frommeteosynth.adaptersor 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_daysof(month, day)on theall_leapcalendar, pooled across all years in the dataset. This is the seasonal counterpart toget_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=0selects the target day alone; anyn_days >= 183selects the whole year.
- Returns:
Subset of the meteorological data for the window, with the same
date_idcolumn thatget_month_subset()adds, plus theday_slotof each record.- Return type:
pd.DataFrame
- Raises:
ValueError – If
(month, day)is not a valid date, orn_daysis negative.
- 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_elevationandIntfall 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 flatREQUIRED_COLStuple 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_strvalidation, 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_leapyear.
- 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
slotsandcenter.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 over1..366, so onlyperiod=366makes 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 theall_leapcalendar.The mapping is leap-independent:
Feb 29is 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_leapmonth 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
monthis outside 1-12, ordayis 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 validall_leapdate.
- 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
slotis outside1..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_daysof(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=0selects the target day alone; anyn_days >= 183selects the whole year.
- Returns:
Sorted array of slots, of length
min(2 * n_days + 1, 366).- Return type:
np.ndarray
- Raises:
ValueError – If
n_daysis 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,dayandhourcolumns from a datetime index.- Parameters:
data (pd.DataFrame) – A frame indexed by a
DatetimeIndex(as returned bypvlib.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,dayandhourcolumns 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)
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.