Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pypi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on:
- main

jobs:

deploy:
if: github.event.pull_request.merged == true
name: Publish to PyPI
runs-on: ubuntu-latest

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ RUN groupadd -g 1729 no && useradd -m -u 1729 -g no no
USER no
WORKDIR /home/no

COPY ./examples /home/no/examples
COPY --exclude=**/__pycache__ ../examples /home/no/examples

ENV VENV=/home/no/venv
RUN python -m venv $VENV
Expand Down
47 changes: 0 additions & 47 deletions docs/custom_timeline.py

This file was deleted.

56 changes: 18 additions & 38 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,113 +30,93 @@ This is provided by:

*neworder*'s timeline is conceptually a sequence of steps that are iterated over (calling the Model's `step` and (optionally) `check` methods at each iteration, plus the `finalise` method at the last time point, which is commonly used to post-process the raw model data at the end of the model run. Timelines should not be incremented in client code, this happens automatically within the model.

The framework is extensible but provides four types of timeline (implemented in C++):
The framework is extensible but provides four types of timeline (3 implemented in C++, one in python):

- `NoTimeline`: an arbitrary one-step timeline which is designed for continuous-time models in which the model evolution is computed in a single step
- `LinearTimeline`: a set of equally-spaced intervals in non-calendar time
- `NumericTimeline`: a fully-customisable non-calendar timeline allowing for unequally-spaced intervals
- `CalendarTimeline`: a timeline based on calendar dates with with (multiples of) daily, monthly or annual intervals
- `CalendarTimeline`: a timeline based on calendar dates with steps defined daily, monthly or annually

``` mermaid
classDiagram
Timeline <|-- NoTimeline
Timeline <|-- LinearTimeline
Timeline <|-- NumericTimeline
Timeline <|-- CalendarTimeline
Timeline <|-- CustomTimeline

class Timeline {
+int index
+bool at_end*
+float dt*
+Any end*
+float nsteps*
+Any start*
+Any time*
+_next() None*
+__repr__() str*
}

class NoTimeline {
+bool at_end
+float dt
+Any end
+float nsteps
+Any start
+Any time
+_next()
+__repr__() str
}

class LinearTimeline {
+bool at_end
+float dt
+Any end
+float nsteps
+Any start
+Any time
+_next()
+__repr__() str
}

class NumericTimeline {
+bool at_end
+float dt
+Any end
+float nsteps
+Any start
+Any time
+_next()
+__repr__() str
}

class CalendarTimeline {
+bool at_end
+float dt
+Any end
+float nsteps
+Any start
+Any time
+_next()
+__repr__() str
}

class CustomTimeline {
+bool at_end
+float dt
+Any end
+float nsteps
+Any start
+Any time
+date end
+date start
+date time
+_next()
+__repr__() str
}
```

!!! note "Calendar Timelines"
- Calendar timelines do not provide intraday resolution
- Monthly increments preserve the day of the month (where possible)
- Daylight savings time adjustments are made which affect time intervals where the interval crosses a DST change
- Time intervals are computed in years, on the basis of a year being 365.2475 days
!!! warning Timeline Limitations

The current implementation of the `neworder.Timeline` interface does not support iteration outside a model. For now, testing a custom implementation requires creating a dummy model to iterate the timeline. A future `neworder` release may refactor timelines in terms of iterables to make development easier.

#### Custom timelines

If none of the supplied timelines are suitable, users can implement their own, deriving from the abstract `neworder.Timeline` base class, which provides an `index` property that should not be overidden. The following properties and methods must be overridden in the subclass:
If none of the supplied timelines are suitable, users can implement their own, deriving from the abstract `neworder.Timeline` base class, which provides an `index` property that should not be overidden.

!!! example "CalendarTimeline"
`neworder.CalendarTimeline` is now implemented in python and can be used as an example of how to implement a custom timeline:

- Steps are defined in terms of `date` (not `datetime`)
- Monthly increments preserve the day of the month (where possible)
- Time intervals are computed in years, using an ACT/365 basis.

The following properties and methods must be overridden in the subclass:

symbol | type | description
-----------|-------------------|---
`at_end` | `bool` property | whether the timeline has reached it's end point
`dt` | `float` property | the size of the current timestep
`end` | `Any` property | the end time of the timeline
`_next` | `None` method | move to the next timestep (for internal use by model, should not normally be called in client code)
`nsteps` | `int` property | the total number of timesteps
`start` | `Any` property | the start time of the timeline
`time` | `Any` property | the current time of the timeline
`__repr__` | `str` method | (optional) a string representation of the object, defaults to the name of the class

As an example, this open-ended numeric timeline starts at zero and asymptotically converges to 1.0:

{{ include_snippet("./docs/custom_timeline.py", show_filename=False) }}

### Spatial Domain

Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ mkdocs-macros-plugin==1.3.7
mkdocs-material==9.6.14
mkdocs-material-extensions==1.3.1
mkdocs-video==1.5.0
requests==2.32.4
requests==2.33.0
2 changes: 1 addition & 1 deletion examples/markov_chain/markov_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@ def step(self) -> None:
self.summary.loc[len(self.summary)] = self.pop.state.value_counts().transpose()

def finalise(self) -> None:
self.summary["t"] = np.linspace(self.timeline.start, self.timeline.end, self.timeline.nsteps + 1)
self.summary["t"] = np.arange(self.timeline.start, self.timeline.end + 1e-8, self.timeline.dt)
self.summary.reset_index(drop=True, inplace=True)
self.summary.fillna(0, inplace=True)
3 changes: 2 additions & 1 deletion examples/people/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
from datetime import date

from dateutil.relativedelta import relativedelta
from population import Population

import neworder
Expand All @@ -20,7 +21,7 @@
out_migration_rate_data = "examples/people/migration-out.csv"

# define the evolution timeline
timeline = neworder.CalendarTimeline(date(2011, 1, 1), date(2051, 1, 1), 1, "y")
timeline = neworder.CalendarTimeline(date(2011, 1, 1), relativedelta(years=1), end=date(2051, 1, 1))

# create the model
population = Population(
Expand Down
2 changes: 1 addition & 1 deletion examples/people/population.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def check(self) -> bool:

neworder.log(
"check OK: time={} size={} mean_age={:.2f}, pct_female={:.2f} net_migration={} ({}-{})".format(
self.timeline.time.date(),
self.timeline.time,
self.size(),
self.mean_age(),
100.0 * self.gender_split(),
Expand Down
5 changes: 1 addition & 4 deletions neworder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
__version__ = importlib.metadata.version("neworder")

from _neworder_core import (
CalendarTimeline,
LinearTimeline,
Model,
MonteCarlo,
Expand All @@ -22,8 +21,6 @@
verbose,
)

# type: ignore
from .domain import Domain, Edge, Space, StateGrid
from .mc import as_np

__all__ = ["as_np", "Domain", "Space", "freethreaded", "thread_id"]
from .timeline import CalendarTimeline
20 changes: 1 addition & 19 deletions neworder/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ A dynamic microsimulation framework";
from __future__ import annotations

import collections.abc
import datetime
import enum
import types
import typing
Expand All @@ -17,6 +16,7 @@ import numpy.typing
from . import df, mpi, stats, time
from .domain import Domain, Edge, Space, StateGrid
from .mc import as_np
from .timeline import CalendarTimeline

__all__: list[str] = [
"CalendarTimeline",
Expand All @@ -43,24 +43,6 @@ __all__: list[str] = [
"StateGrid",
]

class CalendarTimeline(Timeline):
"""

A calendar-based timeline
"""
@typing.overload
def __init__(self, start: datetime.datetime, end: datetime.datetime, step: typing.SupportsInt, unit: str) -> None:
"""
Constructs a calendar-based timeline, given start and end dates, an increment specified as a multiple of days, months or years
"""
@typing.overload
def __init__(self, start: datetime.datetime, step: typing.SupportsInt, unit: str) -> None:
"""
Constructs an open-ended calendar-based timeline, given a start date and an increment specified as a multiple of days, months or years.
NB the model will run until the Model.halt() method is explicitly called (from inside the step() method). Note also that nsteps() will
return -1 for timelines constructed this way
"""

class LinearTimeline(Timeline):
"""

Expand Down
57 changes: 57 additions & 0 deletions neworder/timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from datetime import date

from dateutil.relativedelta import relativedelta

import neworder as no


class CalendarTimeline(no.Timeline):
"""
A timeline representing calendar days. At any given step, `time` returns the date at the *start* of the step
For monthly timesteps, preserves day of month.
Numeric step size is computed using an ACT/365 basis and may vary depending on choice of step.
To preserve day of month for month-based timesteps when day >= 28, explicitly pass day=... to `relativedelta`
"""

def __init__(self, start: date, step: relativedelta, *, end: date | None = None) -> None:
super().__init__()

if end and end <= start:
raise ValueError("end date must be after start date")
if start + step <= start:
raise ValueError("step must be forward in time")

self._start = start
self._end = end
self._current = start
self._step = step

def _next(self) -> date:
if self._end and self._current >= self._end:
raise StopIteration()
self._current += self._step
return self._current

@property
def start(self) -> date:
return self._start

@property
def end(self) -> date | float:
return self._end or no.time.FAR_FUTURE

@property
def time(self) -> date:
return self._current

@property
def dt(self) -> float:
"""Returns year fraction on ACT/365 basis, or 0 if the timeline has ended"""
if self.at_end:
return 0.0
next: date = self._start + (self.index + 1) * self._step
return (next - self._current).days / 365

@property
def at_end(self) -> bool:
return self._end is not None and self._current >= self._end
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dev = [
"requests>=2.32.4",
"types-requests",
"pybind11-stubgen>=2.5.5",
"types-python-dateutil>=2.9.0.20260124",
]

[project.optional-dependencies]
Expand Down
8 changes: 1 addition & 7 deletions src/Module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ PYBIND11_MODULE(_neworder_core, m)
.def_property_readonly("start", &no::Timeline::start, timeline_start_docstr)
.def_property_readonly("end", &no::Timeline::end, timeline_end_docstr)
.def_property_readonly("index", &no::Timeline::index, timeline_index_docstr)
.def_property_readonly("nsteps", &no::Timeline::nsteps, timeline_nsteps_docstr)
//.def_property_readonly("nsteps", &no::Timeline::nsteps, timeline_nsteps_docstr)
.def_property_readonly("dt", &no::Timeline::dt, timeline_dt_docstr)
.def_property_readonly("at_end", &no::Timeline::at_end, timeline_at_end_docstr)
.def("__repr__", &no::Timeline::repr, timeline_repr_docstr);
Expand All @@ -114,12 +114,6 @@ PYBIND11_MODULE(_neworder_core, m)
py::class_<no::NumericTimeline, no::Timeline>(m, "NumericTimeline", numerictimeline_docstr)
.def(py::init<const std::vector<double>&>(), numerictimeline_init_docstr, "times"_a);

py::class_<no::CalendarTimeline, no::Timeline>(m, "CalendarTimeline", calendartimeline_docstr)
.def(py::init<std::chrono::system_clock::time_point, std::chrono::system_clock::time_point, size_t, char>(),
calendartimeline_init_docstr, "start"_a, "end"_a, "step"_a, "unit"_a)
.def(py::init<std::chrono::system_clock::time_point, size_t, char>(), calendartimeline_init_open_docstr,
"start"_a, "step"_a, "unit"_a);

// MC
py::class_<no::MonteCarlo>(m, "MonteCarlo", mc_docstr)
// constructor is NOT exposed to python, can only be created within a model
Expand Down
Loading
Loading