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
- 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.
- 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*12TimePar
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
pbyss.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