Skip to main content

internals.time

internals.time

Durations and rates, each with its unit (ss.days, ss.peryear, ss.probperyear, …), and plotting dates and durations with Matplotlib

Units are kept with the values so that a module, whatever its timestep, reads a parameter correctly, and mistakes such as adding a rate to a duration raise an error.

Hierarchy of TimePars:

TimePar  # All time parameters
├── Duration  # All durations, units of *time*
│   ├── days  # Duration with units of days
│   ├── weeks
│   ├── months
│   ├── years
│   └── datedur  # Calendar durations, used inside Starsim (ss.dates.datedur); whole ss.months() and ss.years() added to a date follow the calendar
└── Rate  # All rates, units of *per* (e.g. per time or per event)
    ├── per  # Probability rates over time (e.g., death rate per year)
    │   ├── perday
    │   ├── perweek
    │   ├── permonth
    │   └── peryear
    ├── prob  # Unitless probability (e.g., probability of death per infection)
    │   ├── probperday
    │   ├── probperweek
    │   ├── probpermonth
    │   └── probperyear
    └── freq  # Number of events (e.g., number of acts per year)
        ├── freqperday
        ├── freqperweek
        ├── freqpermonth
        └── freqperyear

A per and a freq have the same dimension, and differ in what they give times a duration: a per is a hazard, so rate*dt is the probability of the event in dt, and a freq is an expected count, so freq*dt is the number of events in dt (as rate.to_events(dt) is for either). In a distribution (e.g. ss.poisson(ss.freqperyear(3))), a rate is the number per step, except as a probability (the p of ss.bernoulli()).

The units have one-unit instances, ss.day, ss.week, ss.month, and ss.year (e.g. ss.prob(0.1, ss.year)).

Used with: agents and compartments; declared and coded modules

Classes

Name Description
DateConverter Starsim date conversion interface for matplotlib
Duration Base class for durations: isinstance(x, ss.Duration) is true for any duration
FloatYearFormatter Formatter that renders float years as calendar dates
FloatYearLocator Locator to calculate tick positions for float year axes
MockAxis Emulator for Axis.get_view_interval() to return converted values
Rate A value per unit of time, e.g. 2 per day: the base class of rates, probabilities, and frequencies
TimePar Parent class for all TimePars – dur, Rate, etc.
days A duration in days, e.g. ss.days(6)
dimless A plain number that came from multiplying a rate by a duration, e.g. ss.peryear(0.1) * ss.days(3)
freq Class for the number of events (rather than probability) in a specified period
freqperday A number of events per day, e.g. ss.freqperday(2)
freqpermonth A number of events per month, e.g. ss.freqpermonth(2)
freqperweek A number of events per week, e.g. ss.freqperweek(2)
freqperyear A number of events per year, e.g. ss.freqperyear(2)
months A duration in months, e.g. ss.months(6)
per A per represents an instantaneous rate of an event occurring. Rates
peract A probability per act, for a transmission over a network that defines acts (e.g. ss.SexualNet)
perday An instantaneous rate per day, e.g. ss.perday(0.1)
permonth An instantaneous rate per month, e.g. ss.permonth(0.1)
perweek An instantaneous rate per week, e.g. ss.perweek(0.1)
peryear An instantaneous rate per year, e.g. ss.peryear(0.1)
prob prob represents the probability of an event occurring during a
probperday A probability per day, e.g. ss.probperday(0.1)
probpermonth A probability per month, e.g. ss.probpermonth(0.1)
probperweek A probability per week, e.g. ss.probperweek(0.1)
probperyear A probability per year, e.g. ss.probperyear(0.1)
weeks A duration in weeks, e.g. ss.weeks(6)
years A duration in years, e.g. ss.years(5); the default unit

DateConverter

internals.time.DateConverter()

Starsim date conversion interface for matplotlib

It exists so that Matplotlib can plot ss.date and ss.Duration values directly.

This class gets registered with matplotlib as the converter for Starsim date and DateArray types, allowing them to be used directly as axis values. This achieves two outcomes

  1. When Starsim date or DateArray values are plotted, the x-axis values are automatically converted to float years for plotting. If this class is not used, matplotlib will instead convert them to days since 1970-01-01, which is the default behavior for pd.Timestamp.
  2. If the first plotting command used on an axis has date/DateArray as the x-axis, our standard custom date locators and formatters will be used, so that the float years will automatically be labeled with dates.

Together, this means that users can freely plot both Starsim date/DateArray values and plain float years on the same axis without needing any conversions, and they’ll also automatically get date-based ticks without needing to set the locator, even if they are using custom plotting code rather than ss.Result.plot().

Methods

Name Description
axisinfo Specify which locators and formatters should be used if an instance of a registered class is the first one plotted
axisinfo
internals.time.DateConverter.axisinfo(unit, axis)

Specify which locators and formatters should be used if an instance of a registered class is the first one plotted

Duration

internals.time.Duration(value=1, base=None)

Base class for durations: isinstance(x, ss.Duration) is true for any duration

To make one, use ss.years(), ss.days(), etc. (or ss.Duration(3, 'years') for a unit given as a string). They can be interconverted using .to(), e.g. ss.years(3).to('days').

Attributes

Name Description
days The duration in days: an int for a whole number of days (as for pd.Timedelta.days, e.g. for pd.date_range(periods=...)), otherwise a float

Methods

Name Description
arange Construct an array of dur instances
to_dt Convert this duration to a number of timesteps; see TimePar.to_dt()
arange
internals.time.Duration.arange(start, stop, step=1.0, inclusive=True)

Construct an array of dur instances

Parameters
Name Type Description Default
start float / ss.Duration Starting point, e.g., ss.years(0) required
stop float / ss.Duration Ending point, e.g. ss.years(20) required
step float / ss.Duration Step size, e.g. ss.years(2) 1.0
to_dt
internals.time.Duration.to_dt(dt=None)

Convert this duration to a number of timesteps; see TimePar.to_dt()

Parameters
Name Type Description Default
dt ss.Duration the timestep; if None, the module’s dt None

FloatYearFormatter

internals.time.FloatYearFormatter()

Formatter that renders float years as calendar dates

It exists so that a plot against decimal years shows dates on its axis.

This class provides the same formatting as sc.ScirisDateFormatter, but works with axis values that are float years, rather than matplotlib date values (days since 1970-01-01)

FloatYearLocator

internals.time.FloatYearLocator(unit)

Locator to calculate tick positions for float year axes

It exists so that a plot against decimal years gets tick marks at sensible dates.

Matplotlib’s AutoDateLocator is the standard way of calculating date-based tick positions, incorporating a lot of functionality around sensibly setting the tick intervals e.g., to whole months. This class wraps AutoDateLocator to allow it to be used with float year axis values, by converting the float year values to matplotlib date values before calling the locator, and then converting the calculated tick positions back to float years before returning them.

MockAxis

internals.time.MockAxis(axis)

Emulator for Axis.get_view_interval() to return converted values

Matplotlib’s date locators (AutoDateLocator, YearLocator, MonthLocator etc.) call Axis.get_view_interval() to get the current axis limits. These locators all expect the values to be returned in matplotlib dates (i.e., days since 1970-01-01). This helper class is bound to the actual axis, and emulates Axis.get_view_interval() performing the conversion to return the values in the expected units. That way, the Locator instances can be used directly without needing any other changes.

When this class is instantiated, it is bound to an actual Axis instance. The axis instance would have our custom date units and values in floating point years. Then MockAxis.get_view_interval() converts these years to standard matplotlib date values before they get passed to the Matplotlib locators.

Methods

Name Description
get_view_interval Return converted axis limits, as a tuple of (min, max) in matplotlib date units (days since 1970-01-01)
get_view_interval
internals.time.MockAxis.get_view_interval()

Return converted axis limits, as a tuple of (min, max) in matplotlib date units (days since 1970-01-01)

Rate

internals.time.Rate(value, unit=None)

A value per unit of time, e.g. 2 per day: the base class of rates, probabilities, and frequencies

  • self.value - the numerator (e.g., 2) - a scalar float
  • self.unit - the denominator (e.g., 1 day) - a dur object

It exists so that a rate converts correctly to a different unit or timestep.

Attributes

Name Description
rate The instantaneous rate; an alias to self.value, except for ss.prob, where it is -log(1-value)

Methods

Name Description
to Convert this rate to one with a different denominator
to_dt Convert this rate to its per-timestep value; see TimePar.to_dt()
to_events Simple multiplication: calculate the number of events over the time period
to_prob Convert from one time probability to another
to
internals.time.Rate.to(unit)

Convert this rate to one with a different denominator

As well as the rate classes (e.g. ss.permonth), a duration unit can be supplied (e.g. 'month' or ss.months), in which case the matching rate class of the same subtype is used – so ss.peryear(1).to('month') gives permonth(0.0833), while ss.probperyear(1).to('month') gives probpermonth(...).

Note that for ss.prob the conversion is not linear, since a probability has to be converted via the underlying rate: ss.probperyear(0.5).to(ss.probpermonth) gives probpermonth(0.0561), not probpermonth(0.0417). This matches prob.to_prob().

Parameters
Name Type Description Default
unit str / type the rate class or duration unit to convert to, e.g. ss.permonth or ‘month’ required
to_dt
internals.time.Rate.to_dt(dt=None)

Convert this rate to its per-timestep value; see TimePar.to_dt()

Parameters
Name Type Description Default
dt ss.Duration the timestep; if None, the module’s dt None
to_events
internals.time.Rate.to_events(dur=None)

Simple multiplication: calculate the number of events over the time period

Parameters
Name Type Description Default
dur ss.Duration/float the period; if None, the default duration None
to_prob
internals.time.Rate.to_prob(dur=None, scale=1.0)

Convert from one time probability to another

Parameters
Name Type Description Default
dur ss.Duration the duration over which to convert the probability to None
scale float an optional additional multiplicative scale factor to incorporate in the calculation 1.0
Examples
p_month = ss.probpermonth(0.05)
p_year = p_month.to_prob(ss.year) # Slightly less than 0.05*12

TimePar

internals.time.TimePar()

Parent class for all TimePars – dur, Rate, etc.

It holds what durations and rates share: a value, a unit, conversion, and arithmetic.

Methods

Name Description
replace Return a copy of this TimePar with a different value
set_default_dur Set the default duration, e.g. module.dt, so .to_dt() and .to_prob() can be used with no input
to Convert this TimePar to one of a different class
to_array Force conversion to an array
to_base Class method to convert another TimePar object to this TimePar’s base units
to_dt Resolve this timepar against the timestep, returning a plain number of timesteps
replace
internals.time.TimePar.replace(value)

Return a copy of this TimePar with a different value

Copying rather than constructing a new object preserves the base as well as the class, so ss.perweek(0.1).replace(0.2) is a rate per week, not per year. Analogous to ss.date.replace().

Parameters
Name Type Description Default
value float / array the value to use required
Example
ss.years(5).replace(6) # Returns ss.years(6)
set_default_dur
internals.time.TimePar.set_default_dur(dur)

Set the default duration, e.g. module.dt, so .to_dt() and .to_prob() can be used with no input

Parameters
Name Type Description Default
dur ss.Duration the default duration required
to
internals.time.TimePar.to(unit)

Convert this TimePar to one of a different class

Parameters
Name Type Description Default
unit str / type the unit to convert to, e.g. ‘year’ or ss.years required
to_array
internals.time.TimePar.to_array(*args, **kwargs)

Force conversion to an array

to_base
internals.time.TimePar.to_base(other)

Class method to convert another TimePar object to this TimePar’s base units

Parameters
Name Type Description Default
other TimePar the value to convert required
to_dt
internals.time.TimePar.to_dt(dt=None)

Resolve this timepar against the timestep, returning a plain number of timesteps (for a duration) or the per-timestep value (for a rate)

This is the conversion that must happen before a timepar can be used in timestep arithmetic, e.g. when scheduling a future event in a state array. Without it, the unit is silently discarded and the value is interpreted as a number of timesteps regardless of its units.

Parameters
Name Type Description Default
dt ss.Duration the timestep to resolve against; if not supplied, use the module’s dt (set during initialization) None
Examples
# Inside a module with dt = ss.months(1)
self.ti_clearance[uids] = self.ti_infected[uids] + self.pars.dur_persist.to_dt() # ✓ Right
self.ti_clearance[uids] = self.ti_infected[uids] + self.pars.dur_persist # × WRONG -- discards the unit

# Standalone, with an explicit timestep
ss.years(100).to_dt(ss.months(1)) # 1200.0
ss.peryear(0.1).to_dt(ss.months(1)) # 0.00830...  (a per-timestep probability)

days

internals.time.days(value=1, base=None)

A duration in days, e.g. ss.days(6)

dimless

internals.time.dimless()

A plain number that came from multiplying a rate by a duration, e.g. ss.peryear(0.1) * ss.days(3)

It behaves exactly as a NumPy float (and any arithmetic on it gives one), but remembers the two values it came from, so that a rate= or dur= slot can refuse it with a TypeError rather than taking it as a rate or a duration.

freq

internals.time.freq(value, unit=None)

Class for the number of events (rather than probability) in a specified period

The same dimension as ss.per, but by default multiplication returns a number of events rather than a probability: ss.freqperday(0.1) * ss.days(3) is 0.3. Use ss.freq for events (e.g., number of acts per year) and ss.per for hazards (e.g., the rate of death per year); adding one to the other is an error. As the rate of a declared transition, a freq is a hazard, the same as a per of the same value.

freqperday

internals.time.freqperday(value, unit=None)

A number of events per day, e.g. ss.freqperday(2)

freqpermonth

internals.time.freqpermonth(value, unit=None)

A number of events per month, e.g. ss.freqpermonth(2)

freqperweek

internals.time.freqperweek(value, unit=None)

A number of events per week, e.g. ss.freqperweek(2)

freqperyear

internals.time.freqperyear(value, unit=None)

A number of events per year, e.g. ss.freqperyear(2)

months

internals.time.months(value=1, base=None)

A duration in months, e.g. ss.months(6)

per

internals.time.per(value, unit=None)

A per represents an instantaneous rate of an event occurring. Rates must be non-negative, but need not be less than 1.

Through multiplication, rate can be modified or converted to a probability, depending on the data type of the object being multiplied.

When a per is multiplied by a scalar or array, the rate is simply scaled. Such multiplication occurs frequently in epidemiological models, where the base rate is multiplied by “rate ratio” or “relative rate” to represent people experiencing higher (multiplier > 1) or lower (multiplier < 1) event rates.

Alternatively, when a per is multiplied by a duration (type ss.Duration), a probability is calculated. The conversion from rate to probability on multiplication by a duration is 1 - np.exp(-rate*factor), where factor is the ratio of the multiplied duration to the original period (denominator).

For example, consider >>> p = ss.per(0.8, ss.years(1)) When multiplied by a duration of 1 year, the calculated probability is 1 - np.exp(-0.8), which is approximately 55%. >>> p*ss.years(1)

When multiplied by a scalar, the rate is simply scaled. >>> p*2

The difference between prob and per is subtle, but important. per works directly with the instantaneous rate of an event occurring. In contrast, prob starts with a probability and a duration, and the underlying rate is calculated. On multiplication by a duration, * per: rate -> probability * prob: probability -> rate -> probability

The behavior of both classes depends on the data type of the object being multiplied.

ss.per has the same dimension as ss.freq, but by default multiplication returns a probability rather than a number of events. Use ss.per for hazards (e.g., the rate of death per year), and ss.freq for events (e.g., number of acts per year).

peract

internals.time.peract(value=None, **directions)

A probability per act, for a transmission over a network that defines acts (e.g. ss.SexualNet)

It is not a time value: it has no unit and no conversion, so it means the same at any timestep. A partnership with a acts per unit time transmits over a step dt with probability 1 − (1 − p)^(a·dt). Modifiers, rel_trans, rel_sus, and source weights multiply the per-act hazard −ln(1 − p), so beta=0.03 with rel_trans=10 is 1 − 0.97^10 = 0.263 per act.

Parameters

Name Type Description Default
value float / par / dict one probability for every direction of the network; or a dict of a value for each direction, keyed as below None
**directions a value for each direction of the network, named <side>2<side> from the network’s sides (net.directions), e.g. m2f and f2m on ss.SexualNet

Each value is a number in [0, 1], an ss.par, an expression of them, an ss.timeseries, or an ss.by over a dimension of the susceptible. A direction can also be written as a pair of sides, {('m', 'f'): 0.3}, which is the same as m2f=0.3. Every direction of the network needs a value (unless one is given for all), and a direction it does not have raises; both are checked when the sim is initialized. On a route without acts (ss.RandomNet, mixing pools), it raises a ValueError.

Examples

S, I = ss.states(2)
hiv = ss.Disease(infection=S >> I @ dict(sexualnet=ss.peract(m2f=0.001, f2m=0.0005), needles=ss.peract(0.006))*I)
syph = ss.Disease(infection=S >> I @ ss.peract({('m', 'f'): 0.3, ('f', 'm'): 0.15})*I)

Attributes

Name Description
pars The parameters in its values, so they can be fitted
values The values, one or one for each direction

Methods

Name Description
check A probability per act must be in [0, 1]
resolve The value for each direction of a network, in the order of net.directions; checks the keys against them
check
internals.time.peract.check(val, where='')

A probability per act must be in [0, 1]

resolve
internals.time.peract.resolve(net)

The value for each direction of a network, in the order of net.directions; checks the keys against them

Parameters
Name Type Description Default
net ss.Network the network required

perday

internals.time.perday(value, unit=None)

An instantaneous rate per day, e.g. ss.perday(0.1)

permonth

internals.time.permonth(value, unit=None)

An instantaneous rate per month, e.g. ss.permonth(0.1)

perweek

internals.time.perweek(value, unit=None)

An instantaneous rate per week, e.g. ss.perweek(0.1)

peryear

internals.time.peryear(value, unit=None)

An instantaneous rate per year, e.g. ss.peryear(0.1)

prob

internals.time.prob(value=None, unit=None, rate=None)

prob represents the probability of an event occurring during a specified period of time.

The class is designed to allow conversion of a probability from one duration to another through multiplication. However, the behavior of this conversion depends on the data type of the object being multiplied.

When multiplied by a duration (type ss.Duration), the underlying constant rate is calculated as rate = -np.log(1 - self.value). Then, the probability over the new duration is p = 1 - np.exp(-rate*factor), where factor is the ratio of the new duration to the original duration.

For example, >>> p = ss.prob(0.8, ss.years(1)) indicates a 80% chance of an event occurring in one year.

p*ss.years(1) When multiplied by the original denominator, 1 year in this case, the probability remains unchanged, 80%.

p * ss.years(2) Multiplying p by ss.years(2) does not simply double the probability to 160% (which is not possible), but rather returns a new probability of 96% representing the chance of the event occurring at least once over the new duration of two years.

When a prob object is multiplied by a number, the underlying rate is scaled, not the probability, so the result is always a valid probability: >>> p * 2 is ss.prob(0.96, ss.years(1)), the same as p + p (two competing hazards), and p * 0.2 is 0.275, not 0.16. A probability of 1 is an infinite rate, so it stays 1 whatever it is multiplied by; as the rate of a declared transition, it moves everyone in the step (as agents, those who arrive in the step too; in compartments, the whole compartment at the start of each step or substep).

Use per instead of prob if you would prefer to directly specify the instantaneous rate.

probperday

internals.time.probperday(value=None, unit=None, rate=None)

A probability per day, e.g. ss.probperday(0.1)

probpermonth

internals.time.probpermonth(value=None, unit=None, rate=None)

A probability per month, e.g. ss.probpermonth(0.1)

probperweek

internals.time.probperweek(value=None, unit=None, rate=None)

A probability per week, e.g. ss.probperweek(0.1)

probperyear

internals.time.probperyear(value=None, unit=None, rate=None)

A probability per year, e.g. ss.probperyear(0.1)

weeks

internals.time.weeks(value=1, base=None)

A duration in weeks, e.g. ss.weeks(6)

years

internals.time.years(value=1, base=None)

A duration in years, e.g. ss.years(5); the default unit

Functions

Name Description
assume_cal_year Whether a value should be interpreted as a calendar year – by default, a number of 1800 or more
get_dur_class Helper function to get a Dur class
get_rate_class Helper function to get a Rate class
get_unit_class Take a string or class and return the corresponding TimePar class; which is ‘dur’, ‘rate’, or ‘full’ (either)
incompatible Raise a TypeError for two values that cannot be combined, e.g. ss.perday(0.1) + ss.prob(0.1, 'day')
is_number Whether a value is a plain number or an array of them, not e.g. an array of durations
normalize_unit Allow either e.g. ‘year’ or ‘years’ as input – i.e. add an ‘s’
type_str A short description of a value’s type, for error messages, e.g. “a rate (ss.perday)”
warn_ambiguous Warn if a bare number given as a time could be a year or not (above 1000 but below 1800, read as a time from 0)
wrong_dimension Raise a TypeError for arithmetic whose result has a time dimension that no type has, e.g. a rate times a rate

assume_cal_year

internals.time.assume_cal_year(val)

Whether a value should be interpreted as a calendar year – by default, a number of 1800 or more

Parameters

Name Type Description Default
val float/ss.date/ss.Duration the value required

get_dur_class

internals.time.get_dur_class(unit)

Helper function to get a Dur class

Parameters

Name Type Description Default
unit str / type the unit, e.g. ‘year’ or ss.years required

get_rate_class

internals.time.get_rate_class(unit)

Helper function to get a Rate class

Parameters

Name Type Description Default
unit str / type the unit, e.g. ‘peryear’ or ss.peryear required

get_unit_class

internals.time.get_unit_class(which, unit)

Take a string or class and return the corresponding TimePar class; which is ‘dur’, ‘rate’, or ‘full’ (either)

Parameters

Name Type Description Default
which str ‘dur’, ‘rate’, or ‘full’ (either) required
unit str/type/TimePar the unit, e.g. ‘year’ or ss.years required

incompatible

internals.time.incompatible(a, op, b, hint='')

Raise a TypeError for two values that cannot be combined, e.g. ss.perday(0.1) + ss.prob(0.1, 'day')

is_number

internals.time.is_number(val)

Whether a value is a plain number or an array of them, not e.g. an array of durations

normalize_unit

internals.time.normalize_unit(base)

Allow either e.g. ‘year’ or ‘years’ as input – i.e. add an ‘s’

type_str

internals.time.type_str(obj)

A short description of a value’s type, for error messages, e.g. “a rate (ss.perday)”

warn_ambiguous

internals.time.warn_ambiguous(val, name='time')

Warn if a bare number given as a time could be a year or not (above 1000 but below 1800, read as a time from 0)

wrong_dimension

internals.time.wrong_dimension(a, op, b)

Raise a TypeError for arithmetic whose result has a time dimension that no type has, e.g. a rate times a rate