diff --git a/MANIFEST.in b/MANIFEST.in index 55a7df0a..bb7c699c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include LICENCE.md include README.md -include test/*.csv +recursive-exclude test * include src/*.h diff --git a/docs/api.md b/docs/api.md index 6776ea01..51608b9c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,864 +1,6 @@ -# ![module](https://img.shields.io/badge/-module-blue) `neworder` ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `CalendarTimeline` - - -A calendar-based timeline - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `__init__` - -```python -__init__(*args, **kwargs) -``` -Overloaded function. - -```python - __init__(self: neworder.CalendarTimeline, start: datetime.datetime, end: datetime.datetime, step: int, unit: str) -> None -``` - - -Constructs a calendar-based timeline, given start and end dates, an increment specified as a multiple of days, months or years - - -```python - __init__(self: neworder.CalendarTimeline, start: datetime.datetime, step: int, 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 - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `at_end` - -```python -at_end(self: neworder.CalendarTimeline) -> bool -``` - - -Returns True if the current step is the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `dt` - -```python -dt(self: neworder.CalendarTimeline) -> float -``` - - -Returns the step size size of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `end` - -```python -end(self: neworder.CalendarTimeline) -> object -``` - - -Returns the time of the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `index` - -```python -index(self: neworder.CalendarTimeline) -> int -``` - - -Returns the index of the current step in the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `nsteps` - -```python -nsteps(self: neworder.CalendarTimeline) -> int -``` - - -Returns the number of steps in the timeline (or -1 if open-ended) - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `start` - -```python -start(self: neworder.CalendarTimeline) -> object -``` - - -Returns the time of the start of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `time` - -```python -time(self: neworder.CalendarTimeline) -> object -``` - - -Returns the time of the current step in the timeline - - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `Domain` - - -Base class for spatial domains. - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `LinearTimeline` - - -An equally-spaced non-calendar timeline . - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `__init__` - -```python -__init__(*args, **kwargs) -``` -Overloaded function. - -```python - __init__(self: neworder.LinearTimeline, start: float, end: float, nsteps: int) -> None -``` - - -Constructs a timeline from start to end, with the given number of steps. - - -```python - __init__(self: neworder.LinearTimeline, start: float, step: float) -> None -``` - - -Constructs an open-ended timeline give a start value and a step size. 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 - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `at_end` - -```python -at_end(self: neworder.LinearTimeline) -> bool -``` - - -Returns True if the current step is the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `dt` - -```python -dt(self: neworder.LinearTimeline) -> float -``` - - -Returns the step size size of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `end` - -```python -end(self: neworder.LinearTimeline) -> object -``` - - -Returns the time of the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `index` - -```python -index(self: neworder.LinearTimeline) -> int -``` - - -Returns the index of the current step in the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `nsteps` - -```python -nsteps(self: neworder.LinearTimeline) -> int -``` - - -Returns the number of steps in the timeline (or -1 if open-ended) - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `start` - -```python -start(self: neworder.LinearTimeline) -> object -``` - - -Returns the time of the start of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `time` - -```python -time(self: neworder.LinearTimeline) -> object -``` - - -Returns the time of the current step in the timeline - - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `Model` - - -The base model class from which all neworder models should be subclassed - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `__init__` - -```python -__init__(self: neworder.Model, timeline: neworder.Timeline, seeder: function) -> None -``` - - -Constructs a model object with a timeline and a seeder function - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `check` - -```python -check(self: neworder.Model) -> bool -``` - - -User-overridable method used to check internal state at each timestep. -Default behaviour is to simply return True. -Returning False will halt the model run. -This function should not be called directly, it is used by the Model.run() function - -Returns: -True if checks are ok, False otherwise. - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `finalise` - -```python -finalise(self: neworder.Model) -> None -``` - - -User-overridable function for custom processing after the final step in the model run. -Default behaviour does nothing. This function does not need to be called directly, it is called by the Model.run() function - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `halt` - -```python -halt(self: neworder.Model) -> None -``` - - -Signal to the model to stop execution gracefully at the end of the current timestep, e.g. if some convergence criterion has been met, -or input is required from an upstream model. The model can be subsequently resumed by calling the run() function. -For trapping exceptional/error conditions, prefer to raise an exception, or return False from the Model.check() function - - ---- - -### ![property](https://img.shields.io/badge/-property-lightgreen) `mc` - - -The model's Monte-Carlo engine - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `modify` - -```python -modify(self: neworder.Model, r: int) -> None -``` - - -User-overridable method used to modify state in a per-process basis for multiprocess model runs. -Default behaviour is to do nothing. -This function should not be called directly, it is used by the Model.run() function - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `step` - -```python -step(self: neworder.Model) -> None -``` - - -User-implemented method used to advance state of a model. -Default behaviour raises NotImplementedError. -This function should not be called directly, it is used by the Model.run() function - - ---- - -### ![property](https://img.shields.io/badge/-property-lightgreen) `timeline` - - -The model's timeline object - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `MonteCarlo` - - -The model's Monte-Carlo engine with configurable options for parallel execution - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `arrivals` - -```python -arrivals(self: neworder.MonteCarlo, lambda: numpy.ndarray[numpy.float64], dt: float, n: int, mingap: float) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of n arrays of multiple arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with a minimum separation between events of mingap. Sampling uses the Lewis-Shedler "thinning" algorithm -The final value of lambda must be zero, and thus arrivals don't always occur, indicated by a value of neworder.time.never() -The inner dimension of the returned 2d array is governed by the the maximum number of arrivals sampled, and will thus vary - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `counts` - -```python -counts(self: neworder.MonteCarlo, lambda: numpy.ndarray[numpy.float64], dt: float) -> numpy.ndarray[numpy.int64] -``` - - -Returns an array of simulated arrival counts (within time dt) for each intensity in lambda - - -### ![function](https://img.shields.io/badge/-function-red) `deterministic_identical_stream` - -```python -deterministic_identical_stream(r: int) -> int -``` - - -Returns a deterministic seed (19937). Input argument is ignored - - -### ![function](https://img.shields.io/badge/-function-red) `deterministic_independent_stream` - -```python -deterministic_independent_stream(r: int) -> int -``` - - -Returns a deterministic seed that is a function of the input (19937+r). -The model uses the MPI rank as the input argument, allowing for differently seeded streams in each process - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `first_arrival` - -```python -first_arrival(*args, **kwargs) -``` -Overloaded function. - -```python - first_arrival(self: neworder.MonteCarlo, lambda: numpy.ndarray[numpy.float64], dt: float, n: int, minval: float) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of length n of first arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with a minimum start time of minval. Sampling uses the Lewis-Shedler "thinning" algorithm -If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() - - -```python - first_arrival(self: neworder.MonteCarlo, lambda: numpy.ndarray[numpy.float64], dt: float, n: int) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of length n of first arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with no minimum start time. Sampling uses the Lewis-Shedler "thinning" algorithm -If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `hazard` - -```python -hazard(*args, **kwargs) -``` -Overloaded function. - -```python - hazard(self: neworder.MonteCarlo, p: float, n: int) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of ones (with hazard rate lambda) or zeros of length n - - -```python - hazard(self: neworder.MonteCarlo, p: numpy.ndarray[numpy.float64]) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of ones (with hazard rate lambda[i]) or zeros for each element in p - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `next_arrival` - -```python -next_arrival(*args, **kwargs) -``` -Overloaded function. - -```python - next_arrival(self: neworder.MonteCarlo, startingpoints: numpy.ndarray[numpy.float64], lambda: numpy.ndarray[numpy.float64], dt: float, relative: bool, minsep: float) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with start times given by startingpoints with a minimum offset of mingap. Sampling uses the Lewis-Shedler "thinning" algorithm. -If the relative flag is True, then lambda[0] corresponds to start time + mingap, not to absolute time -If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() - - -```python - next_arrival(self: neworder.MonteCarlo, startingpoints: numpy.ndarray[numpy.float64], lambda: numpy.ndarray[numpy.float64], dt: float, relative: bool) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with start times given by startingpoints. Sampling uses the Lewis-Shedler "thinning" algorithm. -If the relative flag is True, then lambda[0] corresponds to start time, not to absolute time -If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() - - -```python - next_arrival(self: neworder.MonteCarlo, startingpoints: numpy.ndarray[numpy.float64], lambda: numpy.ndarray[numpy.float64], dt: float) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), -with start times given by startingpoints. Sampling uses the Lewis-Shedler "thinning" algorithm. -If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() - - -### ![function](https://img.shields.io/badge/-function-red) `nondeterministic_stream` - -```python -nondeterministic_stream(r: int) -> int -``` - - -Returns a random seed from the platform's random_device. Input argument is ignored - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `raw` - -```python -raw(self: neworder.MonteCarlo) -> int -``` - - -Returns a random 64-bit unsigned integer. Useful for seeding other generators. - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `reset` - -```python -reset(self: neworder.MonteCarlo) -> None -``` - - -Resets the generator using the original seed. -Use with care, esp in multi-process models with identical streams - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `sample` - -```python -sample(self: neworder.MonteCarlo, n: int, cat_weights: numpy.ndarray[numpy.float64]) -> numpy.ndarray[numpy.int64] -``` - - -Returns an array of length n containing randomly sampled categorical values, weighted according to cat_weights - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `seed` - -```python -seed(self: neworder.MonteCarlo) -> int -``` - - -Returns the seed used to initialise the random stream - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `state` - -```python -state(self: neworder.MonteCarlo) -> int -``` - - -Returns a hash of the internal state of the generator. Avoids the extra complexity of tranmitting variable-length strings over MPI. - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `stopping` - -```python -stopping(*args, **kwargs) -``` -Overloaded function. - -```python - stopping(self: neworder.MonteCarlo, lambda: float, n: int) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of stopping times (with hazard rate lambda) of length n - - -```python - stopping(self: neworder.MonteCarlo, lambda: numpy.ndarray[numpy.float64]) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of stopping times (with hazard rate lambda[i]) for each element in lambda - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `ustream` - -```python -ustream(self: neworder.MonteCarlo, n: int) -> numpy.ndarray[numpy.float64] -``` - - -Returns an array of uniform random [0,1) variates of length n - - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `NoTimeline` - - -An arbitrary one step timeline, for continuous-time models with no explicit (discrete) timeline - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `__init__` - -```python -__init__(self: neworder.NoTimeline) -> None -``` - - -Constructs an arbitrary one step timeline, where the start and end times are undefined and there is a single step of size zero. Useful for continuous-time models - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `at_end` - -```python -at_end(self: neworder.NoTimeline) -> bool -``` - - -Returns True if the current step is the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `dt` - -```python -dt(self: neworder.NoTimeline) -> float -``` - - -Returns the step size size of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `end` - -```python -end(self: neworder.NoTimeline) -> object -``` - - -Returns the time of the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `index` - -```python -index(self: neworder.NoTimeline) -> int -``` - - -Returns the index of the current step in the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `nsteps` - -```python -nsteps(self: neworder.NoTimeline) -> int -``` - - -Returns the number of steps in the timeline (or -1 if open-ended) - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `start` - -```python -start(self: neworder.NoTimeline) -> object -``` - - -Returns the time of the start of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `time` - -```python -time(self: neworder.NoTimeline) -> object -``` - - -Returns the time of the current step in the timeline - - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `NumericTimeline` - - -An custom non-calendar timeline where the user explicitly specifies the time points, which must be monotonically increasing. - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `__init__` - -```python -__init__(self: neworder.NumericTimeline, times: List[float]) -> None -``` - - -Constructs a timeline from an array of time points. - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `at_end` - -```python -at_end(self: neworder.NumericTimeline) -> bool -``` - - -Returns True if the current step is the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `dt` - -```python -dt(self: neworder.NumericTimeline) -> float -``` - - -Returns the step size size of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `end` - -```python -end(self: neworder.NumericTimeline) -> object -``` - - -Returns the time of the end of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `index` - -```python -index(self: neworder.NumericTimeline) -> int -``` - - -Returns the index of the current step in the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `nsteps` - -```python -nsteps(self: neworder.NumericTimeline) -> int -``` - - -Returns the number of steps in the timeline (or -1 if open-ended) - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `start` - -```python -start(self: neworder.NumericTimeline) -> object -``` - - -Returns the time of the start of the timeline - - -### ![instance method](https://img.shields.io/badge/-instance method-orange) `time` - -```python -time(self: neworder.NumericTimeline) -> object -``` - - -Returns the time of the current step in the timeline - - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `Space` - - -Continuous rectangular n-dimensional finite or infinite domain. -If finite, positioning and/or movement near the domain boundary is -dictated by the `wrap` attribute. - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `StateGrid` - - -Discrete rectangular n-dimensional finite grid domain with each cell having an integer state. -Allows for counting of neighbours according to the supported edge behaviours: -CONSTRAIN (no neighburs over edge), WRAP (toroidal), BOUNCE (reflect) - ---- - -## ![class](https://img.shields.io/badge/-class-darkgreen) `Timeline` - - -`__doc__` empty - ---- - -## ![function](https://img.shields.io/badge/-function-red) `checked` - -```python -checked(checked: bool = True) -> None -``` - - -Sets the checked flag, which determines whether the model runs checks during execution - - ---- - -## ![module](https://img.shields.io/badge/-module-blue) `neworder.df` - - -Submodule for operations involving direct manipulation of pandas dataframes - -### ![function](https://img.shields.io/badge/-function-red) `testfunc` - -```python -testfunc(model: neworder.Model, df: object, colname: str) -> None -``` - - -Test function for direct dataframe manipulation. Results may vary. Do not use. - - -### ![function](https://img.shields.io/badge/-function-red) `transition` - -```python -transition(model: neworder.Model, categories: numpy.ndarray[numpy.int64], transition_matrix: numpy.ndarray[numpy.float64], df: object, colname: str) -> None -``` - - -Randomly changes categorical data in a dataframe, according to supplied transition probabilities. -Args: -model: The model (for access to the MonteCarlo engine). -categories: The set of possible categories -transition_matrix: The probabilities of transitions between categories -df: The dataframe, which is modified in-place -colname: The name of the column in the dataframe - - -### ![function](https://img.shields.io/badge/-function-red) `unique_index` - -```python -unique_index(n: int) -> numpy.ndarray[numpy.int64] -``` - - -Generates an array of n unique values, even across multiple processes, that can be used to unambiguously index multiple dataframes. - - ---- - -## ![module](https://img.shields.io/badge/-module-blue) `neworder.domain` - - -Spatial structures for positioning and moving entities and computing distances - ---- - -### ![class](https://img.shields.io/badge/-class-darkgreen) `Domain` - - -Base class for spatial domains. - ---- - -### ![class](https://img.shields.io/badge/-class-darkgreen) `Space` - - -Continuous rectangular n-dimensional finite or infinite domain. -If finite, positioning and/or movement near the domain boundary is -dictated by the `wrap` attribute. - ---- - -### ![class](https://img.shields.io/badge/-class-darkgreen) `StateGrid` - - -Discrete rectangular n-dimensional finite grid domain with each cell having an integer state. -Allows for counting of neighbours according to the supported edge behaviours: -CONSTRAIN (no neighburs over edge), WRAP (toroidal), BOUNCE (reflect) - ---- - -## ![function](https://img.shields.io/badge/-function-red) `log` - -```python -log(obj: object) -> None -``` - - -The logging function. Prints obj to the console, annotated with process information - - ---- - -## ![module](https://img.shields.io/badge/-module-blue) `neworder.mpi` - - -Submodule for basic MPI environment discovery - -### ![function](https://img.shields.io/badge/-function-red) `rank` - -```python -rank() -> int -``` - - -Returns the MPI rank of the process - - -### ![function](https://img.shields.io/badge/-function-red) `size` - -```python -size() -> int -``` - - -Returns the MPI size (no. of processes) of the run +# API Documentation +!!! note "API documentation is no longer provided here." + As the `neworder` package now contains type hints, your IDE should display these: + ![type hints](./examples/img/apidoc.png) diff --git a/docs/examples/img/apidoc.png b/docs/examples/img/apidoc.png new file mode 100644 index 00000000..33beb7c1 Binary files /dev/null and b/docs/examples/img/apidoc.png differ diff --git a/docs/examples/img/conway.gif b/docs/examples/img/conway.gif index 78ad51c0..593960fa 100644 Binary files a/docs/examples/img/conway.gif and b/docs/examples/img/conway.gif differ diff --git a/docs/examples/neworder-1.1.0-examples-src.tgz b/docs/examples/neworder-1.1.0-examples-src.tgz new file mode 100644 index 00000000..ac96d050 Binary files /dev/null and b/docs/examples/neworder-1.1.0-examples-src.tgz differ diff --git a/docs/examples/neworder-1.1.0-examples-src.zip b/docs/examples/neworder-1.1.0-examples-src.zip new file mode 100644 index 00000000..487c6740 Binary files /dev/null and b/docs/examples/neworder-1.1.0-examples-src.zip differ diff --git a/docs/examples/src.md b/docs/examples/src.md index 1ff20639..0687b2a4 100644 --- a/docs/examples/src.md +++ b/docs/examples/src.md @@ -1,7 +1,7 @@ !!! note "Examples Source Code" Source code for all the examples can be downloaded using one of the the links below: - **[neworder-1.0.1-examples-src.tgz](./neworder-1.0.1-examples-src.tgz)** | **[neworder-1.0.1-examples-src.zip](./neworder-1.0.1-examples-src.zip)** + **[neworder-1.1.0-examples-src.tgz](./neworder-1.1.0-examples-src.tgz)** | **[neworder-1.1.0-examples-src.zip](./neworder-1.1.0-examples-src.zip)** The contained file `requirements.txt` lists the package dependencies of the examples, which can be installed using e.g.: diff --git a/docs/requirements.txt b/docs/requirements.txt index 4aee776e..c6b18492 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # this file is used by readthedocs.io -mkdocs>=1.1.2 -mkdocs-material>=7.1.4 -mkdocs-macros-plugin>=0.5.5 -mkdocs-material-extensions>=1.0.1 +mkdocs>=1.2.3 +mkdocs-material>=8.2.5 +mkdocs-macros-plugin>=0.6.4 +mkdocs-material-extensions>=1.0.3 requests \ No newline at end of file diff --git a/examples/competing/people.py b/examples/competing/people.py index 7c038f23..edf3b159 100644 --- a/examples/competing/people.py +++ b/examples/competing/people.py @@ -1,12 +1,12 @@ import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder as no class People(no.Model): """ A simple aggregration of Persons each represented as a row in a data frame """ - def __init__(self, dt, fertility_hazard_file, mortality_hazard_file, n): + def __init__(self, dt: float, fertility_hazard_file: str, mortality_hazard_file: str, n: int) -> None: super().__init__(no.NoTimeline(), no.MonteCarlo.deterministic_identical_stream) @@ -23,7 +23,7 @@ def __init__(self, dt, fertility_hazard_file, mortality_hazard_file, n): data={"parity": 0, "time_of_death": no.time.far_future()}) - def step(self): + def step(self) -> None: # sample deaths self.population["time_of_death"] = self.mc.first_arrival(self.mortality_hazard.Rate.values, self.dt, len(self.population)) @@ -38,7 +38,7 @@ def step(self): self.population.loc[self.population[col] > self.population.time_of_death, col] = no.time.never() self.population.parity = self.population.parity + ~no.time.isnever(self.population[col].values) - def finalise(self): + def finalise(self) -> None: # compute means no.log("birth rate = %f" % np.mean(self.population.parity)) no.log("percentage mothers = %f" % (100.0 * np.mean(self.population.parity > 0))) diff --git a/examples/competing/visualise.py b/examples/competing/visualise.py index fcda6c08..fca9a9fd 100644 --- a/examples/competing/visualise.py +++ b/examples/competing/visualise.py @@ -1,15 +1,16 @@ import numpy as np -from matplotlib import pyplot as plt +from matplotlib import pyplot as plt # type: ignore +from people import People -def plot(model): +def plot(model: People) -> None: plot_age(model) plt.show() plot_parity(model) plt.show() -def plot_age(model): +def plot_age(model: People) -> None: bins = np.arange(model.max_rate_age) b = [model.population.time_of_baby_1, @@ -26,7 +27,7 @@ def plot_age(model): # plt.savefig("./docs/examples/img/competing_hist_100k.png", dpi=80) -def plot_parity(model): +def plot_parity(model: People) -> None: bins = np.arange(model.population.parity.max()) - 0.25 plt.hist(model.population.parity, bins, width=0.5) plt.title("Births during lifetime") diff --git a/examples/conway/conway.py b/examples/conway/conway.py index 613862d0..a9dc7c45 100644 --- a/examples/conway/conway.py +++ b/examples/conway/conway.py @@ -1,6 +1,7 @@ import numpy as np import neworder as no -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt # type: ignore +from matplotlib.image import AxesImage # type: ignore from matplotlib import colors @@ -8,25 +9,22 @@ class Conway(no.Model): __glider = np.array([[0, 0, 1], [1, 0, 1], [0, 1, 1]], dtype=int) - def __init__(self, nx, ny, n, edge=no.Edge.WRAP): + def __init__(self, nx: int, ny: int, n: int, edge: no.Edge=no.Edge.WRAP) -> None: super().__init__(no.LinearTimeline(0, 1), no.MonteCarlo.nondeterministic_stream) - # create n automata at random positions - rng = np.random.default_rng(self.mc.raw()) - s = rng.choice(np.arange(nx * ny), n, replace=False) - + # create n automata at regular positions init_state = np.zeros((nx * ny)) - for s in s: - init_state[s] = 1 + init_state[::2] = 1 + init_state[::7] = 1 self.domain = no.StateGrid(init_state.reshape(ny, nx), edge=edge) - self.domain.state[20:23, 20:23] = Conway.__glider + #self.domain.state[20:23, 20:23] = Conway.__glider self.fig, self.g = self.__init_visualisation() # !step! - def step(self): + def step(self) -> None: n = self.domain.count_neighbours(lambda x: x > 0) deaths = np.logical_or(n < 2, n > 3) @@ -37,27 +35,27 @@ def step(self): self.__update_visualisation() # !step! - def check(self): - # # randomly place a glider (not across edge) - # if self.timeline.index() == 0: - # x = self.mc.raw() % (self.domain.state.shape[0] - 2) - # y = self.mc.raw() % (self.domain.state.shape[1] - 2) - # self.domain.state[x:x+3, y:y+3] = np.rot90(Conway.__glider, self.mc.raw() % 4) + def check(self) -> bool: + # randomly place a glider (not across edge) + if self.timeline.index() == 0: + x = self.mc.raw() % (self.domain.state.shape[0] - 2) + y = self.mc.raw() % (self.domain.state.shape[1] - 2) + self.domain.state[x:x+3, y:y+3] = np.rot90(Conway.__glider, self.mc.raw() % 4) return True - def __init_visualisation(self): + def __init_visualisation(self) -> tuple[plt.Figure, AxesImage]: plt.ion() cmap = colors.ListedColormap(['black', 'white', 'purple', 'blue', 'green', 'yellow', 'orange', 'red', 'brown']) - fig = plt.figure(constrained_layout=True, figsize=(8, 6)) + fig = plt.figure(constrained_layout=True, figsize=(8, 8)) g = plt.imshow(self.domain.state, cmap=cmap, vmax=9) plt.axis("off") - fig.canvas.mpl_connect('key_press_event', lambda event: self.halt() if event.key == "q" else None) fig.canvas.flush_events() + fig.canvas.mpl_connect('key_press_event', lambda event: self.halt() if event.key == "q" else None) return fig, g - def __update_visualisation(self): + def __update_visualisation(self) -> None: self.g.set_data(self.domain.state) # plt.savefig("/tmp/conway%04d.png" % self.timeline.index(), dpi=80) # if self.timeline.index() > 100: diff --git a/examples/conway/run_model.py b/examples/conway/run_model.py index 130c0cce..04838a02 100644 --- a/examples/conway/run_model.py +++ b/examples/conway/run_model.py @@ -3,7 +3,7 @@ import neworder as no # size of domain -nx, ny = (480, 360) +nx, ny = (320, 320) # saturation (proportion initially alive) sat = 0.36 diff --git a/examples/daisyworld/daisyworld.py b/examples/daisyworld/daisyworld.py index 51dc08b0..e513c8c0 100644 --- a/examples/daisyworld/daisyworld.py +++ b/examples/daisyworld/daisyworld.py @@ -1,9 +1,10 @@ +from matplotlib.image import AxesImage # type: ignore import neworder as no import numpy as np -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt # type: ignore from matplotlib import colors -from scipy import signal +from scipy import signal # type: ignore solar_luminosity = 0.8 @@ -16,13 +17,13 @@ class DaisyWorld(no.Model): MAX_AGE = 25 - DIFF_KERNEL = [ + DIFF_KERNEL = np.array([ [1.0 / 16, 1.0 / 16, 1.0 / 16], [1.0 / 16, 1.0 / 2, 1.0 / 16], [1.0 / 16, 1.0 / 16, 1.0 / 16] - ] + ]) - def __init__(self, gridsize, pct_white, pct_black): + def __init__(self, gridsize: tuple[int, int], pct_white: float, pct_black: float) -> None: super().__init__(no.LinearTimeline(0, 1), no.MonteCarlo.deterministic_independent_stream) p = [pct_white, pct_black, 1 - pct_white - pct_black] @@ -43,7 +44,7 @@ def __init__(self, gridsize, pct_white, pct_black): self.fig, self.img = self.__init_visualisation() - def step(self): + def step(self) -> None: self.age += 1 # update temperature @@ -85,24 +86,24 @@ def step(self): if self.timeline.index() > 3000: self.halt() - def __calc_local_heating(self): + def __calc_local_heating(self) -> np.ndarray[np.float64, np.dtype[np.float64]]: # local_heating = 0 # get absorbed luminosity from state - def fs(state): + def fs(state: np.ndarray[np.int64, np.dtype[np.int64]]) -> np.ndarray[np.float64, np.dtype[np.float64]]: return (1.0 - self.albedo[state]) * solar_luminosity abs_lum = fs(self.domain.state) # get local heating from absorbed luminosity - def fl(lum): + def fl(lum: np.ndarray[np.float64, np.dtype[np.float64]]) -> np.ndarray[np.float64, np.dtype[np.float64]]: return 72.0 * np.log(lum) + 80.0 return fl(abs_lum) - def __diffuse(self): + def __diffuse(self) -> None: padded = np.pad(self.temperature, pad_width=1, mode="wrap") self.temperature = signal.convolve(padded, DaisyWorld.DIFF_KERNEL, mode="same", method="direct")[1:-1, 1:-1] - def __init_visualisation(self): + def __init_visualisation(self) -> tuple[plt.Figure, AxesImage]: # TODO copy wolf-sheep plt.ion() @@ -117,7 +118,7 @@ def __init_visualisation(self): return fig, img - def __update_visualisation(self): + def __update_visualisation(self) -> None: self.img.set_array(self.domain.state.T) # plt.savefig("/tmp/daisyworld%04d.png" % self.timeline.index(), dpi=80) self.fig.canvas.flush_events() diff --git a/examples/markov-chain/markov_chain.py b/examples/markov-chain/markov_chain.py index f0d00086..96b3d103 100644 --- a/examples/markov-chain/markov_chain.py +++ b/examples/markov-chain/markov_chain.py @@ -1,10 +1,10 @@ -import pandas as pd +import pandas as pd # type: ignore import numpy as np import neworder as no class MarkovChain(no.Model): - def __init__(self, timeline, npeople, states, transition_matrix): + def __init__(self, timeline: no.Timeline, npeople: int, states: np.ndarray, transition_matrix: np.ndarray) -> None: super().__init__(timeline, no.MonteCarlo.deterministic_identical_stream) self.npeople = npeople @@ -19,8 +19,8 @@ def __init__(self, timeline, npeople, states, transition_matrix): self.summary = self.summary.append(self.pop.state.value_counts().transpose()) # pure python equivalent implementation of no.df.transition, to illustrate the performance gain - def transition_py(self, colname): - def _interp(cumprob, x): + def transition_py(self, colname: str) -> None: + def _interp(cumprob: np.ndarray, x: float) -> int: lbound = 0 while lbound < len(cumprob) - 1: if cumprob[lbound] > x: @@ -28,7 +28,7 @@ def _interp(cumprob, x): lbound += 1 return lbound - def _sample(u, tc, c): + def _sample(u: float, tc: np.ndarray, c: np.ndarray) -> float: return c[_interp(tc, u)] # u = m.mc.ustream(len(df)) @@ -43,13 +43,13 @@ def _sample(u, tc, c): # this is a much faster equivalent of the loop in the commented code immediately above self.pop[colname] = self.pop[colname].apply(lambda current: _sample(self.mc.ustream(1), tc[lookup[current]], self.states)) - def step(self): + def step(self) -> None: # self.transition_py("state") # comment the above line and uncomment this line to use the faster C++ implementation no.df.transition(self, self.states, self.transition_matrix, self.pop, "state") self.summary = self.summary.append(self.pop.state.value_counts().transpose()) # , ignore_index=True) - def finalise(self): + def finalise(self) -> None: self.summary["t"] = np.linspace(self.timeline.start(), self.timeline.end(), self.timeline.nsteps() + 1) # self.summary.set_index(np.linspace(self.timeline.start(), self.timeline.end(), self.timeline.nsteps() + 1), drop=True, inplace=True) self.summary.reset_index(drop=True, inplace=True) diff --git a/examples/markov-chain/visualisation.py b/examples/markov-chain/visualisation.py index e1947769..90d2dbd5 100644 --- a/examples/markov-chain/visualisation.py +++ b/examples/markov-chain/visualisation.py @@ -1,8 +1,8 @@ -from matplotlib import pyplot as plt +from matplotlib import pyplot as plt # type: ignore +from markov_chain import MarkovChain - -def show(model): +def show(model: MarkovChain) -> None: # this seems to have a bug # model.summary.plot(kind='bar', width=1.0, stacked=True) dt = model.timeline.dt() diff --git a/examples/mortality/people.py b/examples/mortality/people.py index 7f8807b0..764a0a4d 100644 --- a/examples/mortality/people.py +++ b/examples/mortality/people.py @@ -1,13 +1,13 @@ import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder # !disc_ctor! class PeopleDiscrete(neworder.Model): """ Persons sampled each represented as a row in a data frame """ - def __init__(self, mortality_hazard_file, n, max_age): + def __init__(self, mortality_hazard_file: str, n: int, max_age: float) -> None: # This is case-based model the timeline refers to the age of the cohort timeline = neworder.LinearTimeline(0.0, max_age, int(max_age)) super().__init__(timeline, neworder.MonteCarlo.deterministic_identical_stream) @@ -28,7 +28,7 @@ def __init__(self, mortality_hazard_file, n, max_age): # !disc_ctor! # !disc_step! - def step(self): + def step(self) -> None: # kill off some people self.die() # age the living only @@ -36,12 +36,12 @@ def step(self): self.population.loc[alive, "age"] = self.population.loc[alive, "age"] + self.timeline.dt() # !disc_step! - def check(self): + def check(self) -> bool: neworder.log("pct alive = %f" % (100.0 * np.mean(self.population.alive))) return True # !disc_finalise! - def finalise(self): + def finalise(self) -> None: # kill off any survivors self.die() assert np.sum(self.population.alive) == 0 @@ -49,7 +49,7 @@ def finalise(self): self.life_expectancy = np.mean(self.population.age_at_death) # !disc_finalise! - def die(self): + def die(self) -> None: # using indexes to subset data as cannot store a reference to a subset of the dataframe (it just copies) # first filter out the already dead @@ -72,7 +72,7 @@ def die(self): # !cont_ctor! class PeopleContinuous(neworder.Model): """ Persons sampled each represented as a row in a data frame """ - def __init__(self, mortality_hazard_file, n, dt): + def __init__(self, mortality_hazard_file: str, n: int, dt: float) -> None: # Direct sampling doesnt require a timeline super().__init__(neworder.NoTimeline(), neworder.MonteCarlo.deterministic_identical_stream) # initialise cohort @@ -90,17 +90,17 @@ def __init__(self, mortality_hazard_file, n, dt): # !cont_ctor! # !cont_step! - def step(self): + def step(self) -> None: self.population.age_at_death = self.mc.first_arrival(self.mortality_hazard.Rate.values, self.dt, len(self.population)) # !cont_step! # !cont_check! - def check(self): + def check(self) -> bool: # ensure all times of death are finite return self.population.age_at_death.isnull().sum() == 0 # !cont_check! # !cont_finalise! - def finalise(self): + def finalise(self) -> None: self.life_expectancy = np.mean(self.population.age_at_death) # !cont_finalise! diff --git a/examples/mortality/plot.py b/examples/mortality/plot.py index af61c32a..e468a55f 100644 --- a/examples/mortality/plot.py +++ b/examples/mortality/plot.py @@ -1,11 +1,13 @@ - +from typing import Optional, Union import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as animation +import pandas as pd # type: ignore +import matplotlib.pyplot as plt # type: ignore +import matplotlib.animation as animation # type: ignore + class Hist: - def __init__(self, data, numbins): + def __init__(self, data: pd.DataFRame, numbins: int) -> None: fig, ax = plt.subplots() # see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.hist.html @@ -17,14 +19,14 @@ def __init__(self, data, numbins): self.anim = animation.FuncAnimation(fig, self.__animate, interval=100, frames=numbins, repeat=False) - def save(self, filename): + def save(self, filename: str) -> None: # there seems to be no way of preventing passing the loop once setting to the saved gif and it loops forever, which is very annoying self.anim.save(filename, dpi=80, writer=animation.ImageMagickWriter(extra_args=["-loop", "1"])) - def show(self): + def show(self) -> None: plt.show() - def __animate(self, frameno): + def __animate(self, frameno: int) -> Union[list, list[list]]: i = 0 for rect, h in zip(self.patches, self.n): rect.set_height(h if i <= frameno else 0) @@ -32,7 +34,7 @@ def __animate(self, frameno): return self.patches -def plot(pop_disc, pop_cont, filename=None, anim_filename=None): +def plot(pop_disc: pd.DataFrame, pop_cont: pd.DataFrame, filename: Optional[str]=None, anim_filename: Optional[str]=None) -> None: bins = int(max(pop_disc.age_at_death.max(), pop_cont.age_at_death.max())) + 1 rng = (0.0, float(bins)) y1, x1 = np.histogram(pop_disc.age_at_death, bins, range=rng) diff --git a/examples/n-body/n_body.py b/examples/n-body/n_body.py index 394b3d04..4891c67c 100644 --- a/examples/n-body/n_body.py +++ b/examples/n-body/n_body.py @@ -1,15 +1,16 @@ import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder as no from neworder.domain import Space -import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D +import matplotlib.pyplot as plt # type: ignore +from mpl_toolkits.mplot3d import Axes3D # type: ignore +from matplotlib.collections import PathCollection # type: ignore class NBody(no.Model): - def __init__(self, N, G, dt): + def __init__(self, N: int, G: float, dt: float): super().__init__(no.LinearTimeline(0, dt), no.MonteCarlo.deterministic_identical_stream) # unbounded domain @@ -53,7 +54,7 @@ def __init__(self, N, G, dt): self.g = self.__init_visualisation() # also calc energy of system - def __calc_a(self): + def __calc_a(self) -> None: dist2s, _ = self.domain.dists2((self.bodies.x, self.bodies.y, self.bodies.z)) dist2s = np.where(dist2s == 0.0, np.inf, dist2s) @@ -69,19 +70,19 @@ def __calc_a(self): b.ke = b.m * (b.vx ** 2 + b.vy ** 2 + b.vz ** 2) b.pe = -self.G * b.m * np.sum(self.bodies.m / dists[i]) - def __update_pos(self): + def __update_pos(self) -> None: # ignore returned v (will not be altered in an unconstrained domain) (self.bodies.x, self.bodies.y, self.bodies.z), _ = self.domain.move((self.bodies.x, self.bodies.y, self.bodies.z), (self.bodies.vx, self.bodies.vy, self.bodies.vz), self.dt, ungroup=True) - def __update_v(self, frac=1): + def __update_v(self, frac: float=1.0) -> None: dt = self.dt * frac self.bodies.vx += self.bodies.ax * dt self.bodies.vy += self.bodies.ay * dt self.bodies.vz += self.bodies.az * dt - def check(self): + def check(self) -> bool: # check momentum and energy conservation px = np.sum(self.bodies.m * self.bodies.vx) py = np.sum(self.bodies.m * self.bodies.vy) @@ -93,7 +94,7 @@ def check(self): return np.fabs(ke + pe - self.E0) < 20.2 - def step(self): + def step(self) -> None: # 2nd order accurate, see https://medium.com/swlh/create-your-own-n-body-simulation-with-python-f417234885e9 self.__update_v(0.5) self.__update_pos() @@ -103,7 +104,7 @@ def step(self): # _plot(self.bodies) self.__update_visualisation() - def __init_visualisation(self): + def __init_visualisation(self) -> PathCollection: # https://stackoverflow.com/questions/41602588/matplotlib-3d-scatter-animations plt.ion() plt.style.use('dark_background') @@ -127,7 +128,7 @@ def __init_visualisation(self): return g - def __update_visualisation(self): + def __update_visualisation(self) -> None: self.g._offsets3d = (self.bodies.x, self.bodies.y, self.bodies.z) self.fig.canvas.draw() self.fig.canvas.flush_events() diff --git a/examples/option/black_scholes.py b/examples/option/black_scholes.py index 3457dc3b..8b27ea14 100644 --- a/examples/option/black_scholes.py +++ b/examples/option/black_scholes.py @@ -1,17 +1,18 @@ """ Black-Scholes model implementations: analytic and MC """ -import neworder +from typing import Any import numpy as np +from mpi4py import MPI +import neworder from helpers import nstream, norm_cdf -from mpi4py import MPI comm = MPI.COMM_WORLD # Subclass neworder.Model class BlackScholes(neworder.Model): # !constructor! - def __init__(self, option, market, nsims): + def __init__(self, option: dict[str, Any], market: dict[str, float], nsims: int) -> None: # Using exact MC calc of GBM requires only 1 timestep timeline = neworder.LinearTimeline(0.0, option["expiry"], 1) @@ -23,7 +24,7 @@ def __init__(self, option, market, nsims): # !constructor! # !modifier! - def modify(self, rank): + def modify(self, rank: int) -> None: if rank == 1: self.market["spot"] *= 1.01 # delta/gamma up bump elif rank == 2: @@ -33,36 +34,36 @@ def modify(self, rank): # !modifier! # !step! - def step(self): + def step(self) -> None: self.pv = self.simulate() # !step! # !check! - def check(self): + def check(self) -> bool: # check the rng streams are still in sync by sampling from each one, # comparing, and broadcasting the result. If one process fails the # check and exits without notifying the others, deadlocks can result. - # send the state representation to process 0 + # send the state representation to process 0 (others will get None) states = comm.gather(self.mc.state(), 0) # process 0 checks the values - if neworder.mpi.rank() == 0: + if states: ok = all(s == states[0] for s in states) else: - ok = None + ok = True # broadcast process 0's ok to all processes ok = comm.bcast(ok, root=0) return ok # !check! # !finalise! - def finalise(self): + def finalise(self) -> None: # check and report accuracy self.compare() # compute and report some market risk self.greeks() # !finalise! - def simulate(self): + def simulate(self) -> float: # get the single timestep from the timeline dt = self.timeline.dt() normals = nstream(self.mc.ustream(self.nsims)) @@ -82,7 +83,7 @@ def simulate(self): # discount back to val date return fv * np.exp(-r * dt) - def analytic(self): + def analytic(self) -> float: """ Compute Black-Scholes European option price """ S = self.market["spot"] K = self.option["strike"] @@ -105,7 +106,7 @@ def analytic(self): else: return -S * df * norm_cdf(-d1) + K * df * norm_cdf(-d2) - def compare(self): + def compare(self) -> bool: """ Compare MC price to analytic """ ref = self.analytic() err = self.pv / ref - 1.0 @@ -113,12 +114,12 @@ def compare(self): # relative error should be within O(1/(sqrt(sims))) of analytic solution return True if abs(err) <= 2.0 / np.sqrt(self.nsims) else False - def greeks(self): + def greeks(self) -> None: # get all the results pvs = comm.gather(self.pv, 0) # compute sensitivities on rank 0 - if neworder.mpi.rank() == 0: - neworder.log("PV=%f" % pvs[0]) - neworder.log("delta=%f" % ((pvs[1] - pvs[2]) / 2)) - neworder.log("gamma=%f" % ((pvs[1] - 2 * pvs[0] + pvs[2]))) - neworder.log("vega 10bp=%f" % (pvs[3] - pvs[0])) + if pvs: + neworder.log(f"PV={pvs[0]:.3f}") + neworder.log(f"delta={(pvs[1] - pvs[2]) / 2:.3f}") + neworder.log(f"gamma={(pvs[1] - 2 * pvs[0] + pvs[2]):.3f}") + neworder.log(f"vega 10bp={pvs[3] - pvs[0]:.3f}") diff --git a/examples/option/helpers.py b/examples/option/helpers.py index b937c598..f0e39672 100644 --- a/examples/option/helpers.py +++ b/examples/option/helpers.py @@ -1,16 +1,20 @@ - +from typing import TypeVar import numpy as np # for inverse cumulative normal -import scipy.stats -import scipy.special +import scipy.stats # type: ignore +import scipy.special # type: ignore + + +T = TypeVar('T') # Any type. +nparray = np.ndarray[T, np.dtype[T]] -def nstream(u): +def nstream(u: nparray[np.float64]) -> nparray[np.float64]: """ Return a vector of n normally distributed pseudorandom variates (mean zero unit variance) """ return scipy.stats.norm.ppf(u) -def norm_cdf(x): +def norm_cdf(x: nparray[np.float64]) -> nparray[np.float64]: """ Compute the normal cumulatve density funtion """ return (1.0 + scipy.special.erf(x / np.sqrt(2.0))) / 2.0 diff --git a/examples/parallel/parallel.py b/examples/parallel/parallel.py index 3c1f319f..fd3ea255 100644 --- a/examples/parallel/parallel.py +++ b/examples/parallel/parallel.py @@ -1,6 +1,7 @@ # !constructor! this is a tag for inserting code snippets into the documentation +from typing import cast import numpy as np -import pandas as pd +import pandas as pd # type: ignore from mpi4py import MPI comm = MPI.COMM_WORLD @@ -8,7 +9,7 @@ import neworder class Parallel(neworder.Model): - def __init__(self, timeline, p, n): + def __init__(self, timeline: neworder.Timeline, p: float, n: int): # initialise base model (essential!) super().__init__(timeline, neworder.MonteCarlo.nondeterministic_stream) @@ -27,7 +28,7 @@ def __init__(self, timeline, p, n): #!constructor! # !step! - def step(self): + def step(self) -> None: # generate some movement neworder.df.transition(self, self.s, self.p, self.pop, "state") @@ -47,29 +48,28 @@ def step(self): immigrants = comm.recv(source=s) if len(immigrants): neworder.log("received %d immigrants from %d" % (len(immigrants), s)) - self.pop = self.pop.append(immigrants) + self.pop = pd.concat((self.pop, immigrants)) # !step! # !check! - def check(self): + def check(self) -> bool: # Ensure we haven't lost (or gained) anybody totals = comm.gather(len(self.pop), root=0) - if neworder.mpi.rank() == 0: + if totals: if sum(totals) != self.n * neworder.mpi.size(): return False # And check each process only has individuals that it should have out_of_place = comm.gather(len(self.pop[self.pop.state != neworder.mpi.rank()])) - if neworder.mpi.rank() == 0: - if any(out_of_place): - return False + if out_of_place and any(out_of_place): + return False return True # !check! # !finalise! - def finalise(self): + def finalise(self) -> None: # process 0 assembles all the data and prints a summary pops = comm.gather(self.pop, root=0) - if neworder.mpi.rank() == 0: - pops = pd.concat(pops) - neworder.log("State counts (total %d):\n%s" % (len(pops), pops["state"].value_counts().to_string())) + if pops: + pop = pd.concat(pops) + neworder.log("State counts (total %d):\n%s" % (len(pop), pop["state"].value_counts().to_string())) # !finalise! diff --git a/examples/people/population.py b/examples/people/population.py index 2e3b2e21..07b2589e 100644 --- a/examples/people/population.py +++ b/examples/people/population.py @@ -3,14 +3,20 @@ """ import os -import pandas as pd +import pandas as pd # type: ignore import numpy as np import neworder import pyramid class Population(neworder.Model): - def __init__(self, timeline, population_file, fertility_file, mortality_file, in_migration_file, out_migration_file): + def __init__(self, + timeline: neworder.Timeline, + population_file: str, + fertility_file: str, + mortality_file: str, + in_migration_file: str, + out_migration_file: str): super().__init__(timeline, neworder.MonteCarlo.deterministic_identical_stream) @@ -38,23 +44,23 @@ def __init__(self, timeline, population_file, fertility_file, mortality_file, in self.fig = None self.plot_pyramid() - def step(self): + def step(self) -> None: self.births() self.deaths() self.migrations() self.age() - def finalise(self): + def finalise(self) -> None: pass - def age(self): + def age(self) -> None: # Increment age by timestep and update census age category (used for ASFR/ASMR lookup) # NB census age category max value is 86 (=85 or over) self.population.Age = self.population.Age + 1 # NB self.timeline.dt() wont be exactly 1 as based on an average length year of 365.2475 days # reconstruct census age group self.population.DC1117EW_C_AGE = np.clip(np.ceil(self.population.Age), 1, 86).astype(int) - def births(self): + def births(self) -> None: # First consider only females females = self.population[self.population.DC1117EW_C_SEX == 2].copy() @@ -71,9 +77,9 @@ def births(self): newborns.DC1117EW_C_AGE = 1 # this is 0-1 in census category newborns.DC1117EW_C_SEX = 1 + self.mc.hazard(0.5, len(newborns)).astype(int) # 1=M, 2=F - self.population = self.population.append(newborns) + self.population = pd.concat((self.population, newborns)) - def deaths(self): + def deaths(self) -> None: # Map the appropriate mortality rate to each person # might be a more efficient way of generating this array rates = self.population.join(self.mortality, on=["NewEthpop_ETH", "DC1117EW_C_SEX", "DC1117EW_C_AGE"])["Rate"] @@ -84,7 +90,7 @@ def deaths(self): # Finally remove deceased from table self.population = self.population[h!=1] - def migrations(self): + def migrations(self) -> None: # immigration: # - sample counts of migrants according to intensity @@ -101,25 +107,22 @@ def migrations(self): out_rates = self.population.join(self.out_migration, on=["NewEthpop_ETH", "DC1117EW_C_SEX", "DC1117EW_C_AGE"])["Rate"].values h_out = self.mc.hazard(out_rates * self.timeline.dt()) # add incoming & remove outgoing migrants - self.population = self.population[h_out!=1].append(h_in) + self.population = pd.concat((self.population[h_out!=1], h_in)) # record net migration self.in_out = (len(h_in), h_out.sum()) - def mean_age(self): + def mean_age(self) -> float: return self.population.Age.mean() - def gender_split(self): + def gender_split(self) -> float: # this is % female return self.population.DC1117EW_C_SEX.mean() - 1.0 - def net_migration(): - return self.inout[0] - self.in_out[1] + self.in_out[2] - self.in_out[3] - - def size(self): + def size(self) -> int: return len(self.population) - def check(self): + def check(self) -> bool: """ State of the nation """ # check no duplicated unique indices if len(self.population[self.population.index.duplicated(keep=False)]): @@ -147,8 +150,8 @@ def check(self): return True # Faith - def plot_pyramid(self): - a = range(86) + def plot_pyramid(self) -> None: + a = np.arange(86) s = self.population.groupby(by=["DC1117EW_C_SEX", "DC1117EW_C_AGE"])["DC1117EW_C_SEX"].count() m = s[s.index.isin([1], level="DC1117EW_C_SEX")].values f = s[s.index.isin([2], level="DC1117EW_C_SEX")].values diff --git a/examples/people/pyramid.py b/examples/people/pyramid.py index 36aa9a61..45fcff82 100644 --- a/examples/people/pyramid.py +++ b/examples/people/pyramid.py @@ -1,12 +1,13 @@ - """ pyramid plots """ - -import matplotlib.pyplot as plt - -import matplotlib.animation as anim +from typing import Any +import numpy as np +import matplotlib.pyplot as plt # type: ignore +import matplotlib.animation as anim # type: ignore # see https://stackoverflow.com/questions/27694221/using-python-libraries-to-plot-two-horizontal-bar-charts-sharing-same-y-axis -def plot(ages, males, females): +def plot(ages: np.ndarray[np.int64, np.dtype[np.int64]], + males: np.ndarray[np.float64, np.dtype[np.float64]], + females: np.ndarray[np.float64, np.dtype[np.float64]]) -> tuple[plt.Figure, plt.Axes, Any, Any]: xmax = 4000 #max(max(males), max(females)) @@ -32,11 +33,14 @@ def plot(ages, males, females): return fig, axes, mbar, fbar -def update(title, fig, axes, mbar, fbar, ages, males, females): - # mbar.remove() - # mbar = axes[0].barh(ages, males, align='center', color='blue') - # fbar.remove() - # fbar = axes[1].barh(ages, females, align='center', color='red') +def update(title: str, + fig: plt.Figure, + axes: plt.Axes, + mbar: Any, + fbar: Any, + ages: np.ndarray[np.int64, np.dtype[np.int64]], + males: np.ndarray[np.float64, np.dtype[np.float64]], + females: np.ndarray[np.float64, np.dtype[np.float64]]) -> tuple[Any, Any]: for rect, h in zip(mbar, males): rect.set_width(h) for rect, h in zip(fbar, females): @@ -47,59 +51,7 @@ def update(title, fig, axes, mbar, fbar, ages, males, females): plt.pause(0.1) return mbar, fbar -def hist(a): - plt.hist(a, bins=range(120)) - plt.show() - -# class Pyramid: -# def __init__(self, ages, males, females): - -# fig, axes, mbar, fbar = plot(ages, males[0], females[0]) - -# self.anim = animation.FuncAnimation(fig, self.__animate, interval=100, frames=numbins, repeat=False) - -# def save(self, filename): -# # there seems to be no way of preventing passing the loop once setting to the saved gif and it loops forever, which is very annoying -# self.anim.save(filename, dpi=80, writer=animation.ImageMagickWriter(extra_args=["-loop", "1"])) - -# # def show(self): -# # plt.show() - -# def __animate(self, frameno): -# i = 0 -# for rect, h in zip(self.patches, self.n): -# rect.set_height(h if i <= frameno else 0) -# i = i + 1 -# return self.patches - - -# def animated(ages, males, females): -# fig, axes, mbar, fbar = plot(ages, males[0], females[0]) - -# def animate(i): -# #print(type(axes[0]), dir(axes[0])) -# #axes[0].remove() -# # for bar in fig.subplots(): -# # bar.remove() -# # mbar = axes[0].barh(ages, males[i], align='center', color='blue') -# # fbar = axes[1].barh(ages, females[i], align='center', color='red') -# for rect, y in zip(mbar, males[i]): -# rect.set_width(y) -# for rect, y in zip(fbar, females[i]): -# rect.set_width(y) - -# #axes[0].set_data(males[i]) -# #print(type(mbar), dir(mbar)) -# #axes[0].set_height(males[i]) -# fig.suptitle(str(i+2011)) -# plt.pause(0.1) - -# #update(str(i+2011), fig, axes, mbar, fbar, ages, males[i], females[i]) - -# animator = anim.FuncAnimation(fig, animate, frames=41, interval=1, repeat=False) - - +# def hist(a): +# plt.hist(a, bins=range(120)) # plt.show() -# animator.save("./pyramid.gif", dpi=80, writer=anim.ImageMagickWriter()) #extra_args=["-loop", "1"])) -# #animator.save("./pyramid.gif", dpi=80, writer=anim.FFMpegFileWriter()) -# #plt.pause(1) + diff --git a/examples/riskpaths/data.py b/examples/riskpaths/data.py index d9bc319f..7910e78e 100644 --- a/examples/riskpaths/data.py +++ b/examples/riskpaths/data.py @@ -16,12 +16,12 @@ class Parity(Enum): PREGNANT = 1 -def partition(start, finish, step=1): +def partition(start: float, finish: float, step: float=1.) -> np.ndarray[np.float64, np.dtype[np.float64]]: """ Helper function to return an inclusive equal-spaced range, i.e. finish will be the last element """ # ensure finish is always included - return np.append(np .arange(start, finish, step), finish) + return np.append(np.arange(start, finish, step), finish) -# Dynamics parameters +# Dynamics parameters # Age of Consent at which the fertility rates begin min_age = 15.0 @@ -43,7 +43,7 @@ def partition(start, finish, step=1): # fertility rates given in 2.5y chunks from 15 to 40 incl fertility_delta_t = 2.5 -AgeintState = partition(min_age, 40, fertility_delta_t) +AgeintState = partition(min_age, 40.0, fertility_delta_t) # // Age baseline for first pregnancy # double AgeBaselinePreg1[AGEINT_STATE] = { diff --git a/examples/riskpaths/riskpaths.py b/examples/riskpaths/riskpaths.py index 8de3d421..d6dc3816 100644 --- a/examples/riskpaths/riskpaths.py +++ b/examples/riskpaths/riskpaths.py @@ -1,7 +1,7 @@ """ RiskPaths model """ import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder # dynamics data @@ -10,7 +10,7 @@ # !ctor! class RiskPaths(neworder.Model): - def __init__(self, n): + def __init__(self, n: int): super().__init__(neworder.NoTimeline(), neworder.MonteCarlo.deterministic_identical_stream) @@ -24,7 +24,7 @@ def __init__(self, n): # !ctor! # !step! - def step(self): + def step(self) -> None: # first sample union state transitions, which influence pregnancy self.__union() @@ -32,7 +32,7 @@ def step(self): self.__pregnancy() # !step! - def __union(self): + def __union(self) -> None: dt_u = data.union_delta_t @@ -57,12 +57,12 @@ def __union(self): + (~neworder.time.isnever(self.population["T_Union2Start"].values)).astype(int) # !finalise! - def finalise(self): + def finalise(self) -> None: neworder.log("mean unions = %f" % np.mean(self.population.Unions)) neworder.log("pregnancy ratio = %f" % np.mean(self.population.Parity == Parity.PREGNANT)) # !finalise! - def __pregnancy(self): + def __pregnancy(self) -> None: # We're interested in the first pregnancy that occurs for each individual # fmin ignores nan (np.minimum is a problem as it doesnt deal with nan well) diff --git a/examples/riskpaths/visualisation.py b/examples/riskpaths/visualisation.py index f6c1e31a..c139031b 100644 --- a/examples/riskpaths/visualisation.py +++ b/examples/riskpaths/visualisation.py @@ -1,9 +1,8 @@ - -from matplotlib import pyplot as plt +from matplotlib import pyplot as plt # type: ignore import neworder from data import min_age, max_age -def plot(model): +def plot(model: neworder.Model) -> None: bins=range(int(min_age),int(max_age)+1) diff --git a/examples/schelling/model.py b/examples/schelling/model.py index 01e6b545..6b197d93 100644 --- a/examples/schelling/model.py +++ b/examples/schelling/model.py @@ -5,7 +5,7 @@ #neworder.verbose() # category 0 is empty -gridsize = [480,360] +gridsize = (480,360) categories = np.array([0.36, 0.12, 0.12, 0.4]) # normalise if necessary # categories = categories / sum(categories) diff --git a/examples/schelling/schelling.py b/examples/schelling/schelling.py index 492580af..06bb924a 100644 --- a/examples/schelling/schelling.py +++ b/examples/schelling/schelling.py @@ -1,11 +1,16 @@ import numpy as np import neworder -import matplotlib.pyplot as plt -from matplotlib import colors +import matplotlib.pyplot as plt # type: ignore +from matplotlib.image import AxesImage # type: ignore +from matplotlib import colors # type: ignore class Schelling(neworder.Model): - def __init__(self, timeline, gridsize, categories, similarity): + def __init__(self, + timeline: neworder.Timeline, + gridsize: tuple[int, int], + categories: np.ndarray[np.float64, np.dtype[np.float64]], + similarity: float) -> None: # NB missing this line can cause memory corruption super().__init__(timeline, neworder.MonteCarlo.deterministic_identical_stream) @@ -20,7 +25,7 @@ def __init__(self, timeline, gridsize, categories, similarity): self.fig, self.img = self.__init_visualisation() - def step(self): + def step(self) -> None: # start with empty cells being satisfied self.sat = (self.domain.state == 0) @@ -63,10 +68,10 @@ def step(self): self.finalise() # !halt! - def finalise(self): + def finalise(self) -> None: plt.pause(5.0) - def __init_visualisation(self): + def __init_visualisation(self) -> tuple[plt.Figure, AxesImage]: plt.ion() cmap = colors.ListedColormap(['white', 'red', 'blue', 'green', 'yellow'][:self.ncategories]) @@ -79,7 +84,7 @@ def __init_visualisation(self): return fig, img - def __update_visualisation(self): + def __update_visualisation(self) -> None: self.img.set_array(self.domain.state.T) # plt.savefig("/tmp/schelling%04d.png" % self.timeline.index(), dpi=80) self.fig.canvas.flush_events() diff --git a/examples/wolf-sheep/model.py b/examples/wolf-sheep/model.py index ce1063ca..8e21ce44 100644 --- a/examples/wolf-sheep/model.py +++ b/examples/wolf-sheep/model.py @@ -1,7 +1,6 @@ import neworder as no from wolf_sheep import WolfSheep -import matplotlib.pyplot as plt #no.verbose() diff --git a/examples/wolf-sheep/wolf_sheep.py b/examples/wolf-sheep/wolf_sheep.py index 4de341cc..7c1d04ad 100644 --- a/examples/wolf-sheep/wolf_sheep.py +++ b/examples/wolf-sheep/wolf_sheep.py @@ -1,10 +1,10 @@ -from neworder.domain import Domain +from typing import Any, Tuple import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder as no -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt # type: ignore WOLF_COLOUR = "black" SHEEP_COLOUR = "red" @@ -12,13 +12,13 @@ class WolfSheep(no.Model): - def __init__(self, params): + def __init__(self, params: dict[str, Any]) -> None: super().__init__(no.LinearTimeline(0.0, 1.0), no.MonteCarlo.deterministic_independent_stream) # hard-coded to unit timestep self.width = params["grid"]["width"] self.height = params["grid"]["height"] - self.domain = no.Space(np.array([0,0]), np.array([self.width, self.height]), edge=Edge.WRAP) + self.domain = no.Space(np.array([0,0]), np.array([self.width, self.height]), edge=no.Edge.WRAP) n_wolves = params["wolves"]["starting_population"] n_sheep =params["sheep"]["starting_population"] @@ -90,14 +90,14 @@ def __init__(self, params): # seed numpy random generator using our generator (for reproducible normal samples) self.npgen = np.random.default_rng(self.mc.raw()) - def step(self): + def step(self) -> None: # step each population self.__step_grass() self.__step_wolves() self.__step_sheep() - def check(self): + def check(self) -> bool: # record data self.t.append(self.timeline.index()) self.wolf_pop.append(len(self.wolves)) @@ -115,11 +115,11 @@ def check(self): self.halt() return True - def __step_grass(self): + def __step_grass(self) -> None: # grow grass self.grass.countdown = np.clip(self.grass.countdown-1, 0, None) - def __step_wolves(self): + def __step_wolves(self) -> None: # move wolves (wrapped) and update cell vx = (2 * self.mc.ustream(len(self.wolves)) - 1.0) * self.wolves.speed vy = (2 * self.mc.ustream(len(self.wolves)) - 1.0) * self.wolves.speed @@ -144,9 +144,9 @@ def __step_wolves(self): cubs = self.wolves[m == 1].copy().set_index(no.df.unique_index(int(sum(m)))) # evolve speed/burn rate from mother + random factor (don't allow to go <=0) should probably be lognormal cubs.speed = np.maximum(cubs.speed + self.npgen.normal(0.0, self.wolf_speed_stddev, len(cubs)), 0.1) - self.wolves = self.wolves.append(cubs) + self.wolves = pd.concat((self.wolves, cubs)) - def __step_sheep(self): + def __step_sheep(self) -> None: # move sheep randomly (wrapped) vx = (2 * self.mc.ustream(len(self.sheep)) - 1.0) * self.sheep.speed vy = (2 * self.mc.ustream(len(self.sheep)) - 1.0) * self.sheep.speed @@ -168,13 +168,13 @@ def __step_sheep(self): lambs = self.sheep[m == 1].copy().set_index(no.df.unique_index(int(sum(m)))) # evolve speed from mother + random factor lambs.speed = np.maximum(lambs.speed + self.npgen.normal(0.0, self.sheep_speed_stddev, len(lambs)), 0.1) - self.sheep = self.sheep.append(lambs) + self.sheep = pd.concat((self.sheep, lambs)) - def __assign_cell(self, agents): + def __assign_cell(self, agents: pd.DataFrame) -> None: # not ints for some reason agents["cell"] = (agents.x.astype(int) + self.width * agents.y.astype(int)).astype(int) - def __init_plot(self): + def __init_plot(self) -> Tuple[Any, ...]: plt.ion() @@ -238,7 +238,7 @@ def __init_plot(self): ax4, ax_ss, b_ss - def __update_plot(self): + def __update_plot(self) -> None: self.ax_g.set_data(np.flip(self.grass.countdown.values.reshape(self.height, self.width), axis=0)) self.ax_w.set_offsets(np.c_[self.wolves.x, self.wolves.y]) self.ax_s.set_offsets(np.c_[self.sheep.x, self.sheep.y]) diff --git a/neworder/__init__.py b/neworder/__init__.py index a24170d5..61fb080f 100644 --- a/neworder/__init__.py +++ b/neworder/__init__.py @@ -1,4 +1,13 @@ -__version__ = "1.0.2" +__version__ = "1.1.0" -from _neworder_core import Model, MonteCarlo, Timeline, NoTimeline, LinearTimeline, NumericTimeline, CalendarTimeline, time, df, mpi, log, run, stats, checked, verbose # type: ignore +from _neworder_core import ( + Model, + MonteCarlo, + Timeline, + NoTimeline, + LinearTimeline, + NumericTimeline, + CalendarTimeline, + time, df, mpi, log, run, stats, checked, verbose +) # type: ignore from .domain import Edge, Domain, Space, StateGrid diff --git a/neworder/__init__.pyi b/neworder/__init__.pyi new file mode 100644 index 00000000..d6cf8b1e --- /dev/null +++ b/neworder/__init__.pyi @@ -0,0 +1,411 @@ +""" +A dynamic microsimulation framework"; +""" +from __future__ import annotations +from typing import overload, TypeVar +import datetime +import numpy as np + +import df +import mpi +from .time import * +import stats +from .domain import * + +T = TypeVar("T") +nparray = np.ndarray[T, np.dtype[T]] + +__all__ = [ + "CalendarTimeline", + "LinearTimeline", + "Model", + "MonteCarlo", + "NoTimeline", + "NumericTimeline", + "Timeline", + "checked", + "df", + "log", + "mpi", + "run", + "stats", + "time", + "verbose" +] + + +class Timeline(): + pass +class LinearTimeline(Timeline): + """ + An equally-spaced non-calendar timeline . + """ + @overload + def __init__(self, start: float, end: float, nsteps: int) -> None: + """ + Constructs a timeline from start to end, with the given number of steps. + + + Constructs an open-ended timeline give a start value and a step size. 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 + """ + @overload + def __init__(self, start: float, step: float) -> None: ... + def __repr__(self) -> str: + """ + Prints a human-readable representation of the timeline + """ + def at_end(self) -> bool: + """ + Returns True if the current step is the end of the timeline + """ + def dt(self) -> float: + """ + Returns the step size size of the timeline + """ + def end(self) -> object: + """ + Returns the time of the end of the timeline + """ + def index(self) -> int: + """ + Returns the index of the current step in the timeline + """ + def nsteps(self) -> int: + """ + Returns the number of steps in the timeline (or -1 if open-ended) + """ + def start(self) -> object: + """ + Returns the time of the start of the timeline + """ + def time(self) -> object: + """ + Returns the time of the current step in the timeline + """ + pass +class Model(): + """ + The base model class from which all neworder models should be subclassed + """ + def __init__(self, timeline: Timeline, seeder: function) -> None: + """ + Constructs a model object with a timeline and a seeder function + """ + def check(self) -> bool: + """ + User-overridable method used to check internal state at each timestep. + Default behaviour is to simply return True. + Returning False will halt the model run. + This function should not be called directly, it is used by the Model.run() function + + Returns: + True if checks are ok, False otherwise. + """ + def finalise(self) -> None: + """ + User-overridable function for custom processing after the final step in the model run. + Default behaviour does nothing. This function does not need to be called directly, it is called by the Model.run() function + """ + def halt(self) -> None: + """ + Signal to the model to stop execution gracefully at the end of the current timestep, e.g. if some convergence criterion has been met, + or input is required from an upstream model. The model can be subsequently resumed by calling the run() function. + For trapping exceptional/error conditions, prefer to raise an exception, or return False from the Model.check() function + """ + def modify(self, r: int) -> None: + """ + User-overridable method used to modify state in a per-process basis for multiprocess model runs. + Default behaviour is to do nothing. + This function should not be called directly, it is used by the Model.run() function + """ + def step(self) -> None: + """ + User-implemented method used to advance state of a model. + Default behaviour raises NotImplementedError. + This function should not be called directly, it is used by the Model.run() function + """ + @property + def mc(self) -> MonteCarlo: + """ + The model's Monte-Carlo engine + + :type: no::MonteCarlo + """ + @property + def timeline(self) -> Timeline: + """ + The model's timeline object + + :type: Timeline + """ + pass +class MonteCarlo(): + """ + The model's Monte-Carlo engine with configurable options for parallel execution + """ + def __repr__(self) -> str: + """ + Prints a human-readable representation of the MonteCarlo engine + """ + def arrivals(self, lambda_: nparray[np.float64], dt: float, n: int, mingap: float) -> nparray[np.float64]: + """ + Returns an array of n arrays of multiple arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with a minimum separation between events of mingap. Sampling uses the Lewis-Shedler "thinning" algorithm + The final value of lambda must be zero, and thus arrivals don't always occur, indicated by a value of neworder.time.never() + The inner dimension of the returned 2d array is governed by the the maximum number of arrivals sampled, and will thus vary + """ + def counts(self, lambda_: nparray[np.float64], dt: float) -> nparray[np.int64]: + """ + Returns an array of simulated arrival counts (within time dt) for each intensity in lambda + """ + @staticmethod + def deterministic_identical_stream(r: int) -> int: + """ + Returns a deterministic seed (19937). Input argument is ignored + """ + @staticmethod + def deterministic_independent_stream(r: int) -> int: + """ + Returns a deterministic seed that is a function of the input (19937+r). + The model uses the MPI rank as the input argument, allowing for differently seeded streams in each process + """ + @overload + def first_arrival(self, lambda_: nparray[np.float64], dt: float, n: int) -> nparray[np.float64]: + """ + Returns an array of length n of first arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with a minimum start time of minval. Sampling uses the Lewis-Shedler "thinning" algorithm + If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() + + + Returns an array of length n of first arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with no minimum start time. Sampling uses the Lewis-Shedler "thinning" algorithm + If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() + """ + @overload + def first_arrival(self, lambda_: nparray[np.float64], dt: float, n: int, minval: float) -> nparray[np.float64]: ... + @overload + def hazard(self, p: float, n: int) -> nparray[np.float64]: + """ + Returns an array of ones (with hazard rate lambda) or zeros of length n + + + Returns an array of ones (with hazard rate lambda[i]) or zeros for each element in p + """ + @overload + def hazard(self, p: nparray[np.float64]) -> nparray[np.float64]: ... + @overload + def next_arrival(self, startingpoints: nparray[np.float64], lambda_: nparray[np.float64], dt: float) -> nparray[np.float64]: + """ + Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with start times given by startingpoints with a minimum offset of mingap. Sampling uses the Lewis-Shedler "thinning" algorithm. + If the relative flag is True, then lambda[0] corresponds to start time + mingap, not to absolute time + If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() + + + Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with start times given by startingpoints. Sampling uses the Lewis-Shedler "thinning" algorithm. + If the relative flag is True, then lambda[0] corresponds to start time, not to absolute time + If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() + + + Returns an array of length n of subsequent arrival times from a nonhomogeneous Poisson process (with hazard rate lambda[i], time interval dt), + with start times given by startingpoints. Sampling uses the Lewis-Shedler "thinning" algorithm. + If the final value of lambda is zero, no arrival is indicated by a value of neworder.time.never() + """ + @overload + def next_arrival(self, startingpoints: nparray[np.float64], lambda_: nparray[np.float64], dt: float, relative: bool) -> nparray[np.float64]: ... + @overload + def next_arrival(self, startingpoints: nparray[np.float64], lambda_: nparray[np.float64], dt: float, relative: bool, minsep: float) -> nparray[np.float64]: ... + @staticmethod + def nondeterministic_stream(r: int) -> int: + """ + Returns a random seed from the platform's random_device. Input argument is ignored + """ + def raw(self) -> int: + """ + Returns a random 64-bit unsigned integer. Useful for seeding other generators. + """ + def reset(self) -> None: + """ + Resets the generator using the original seed. + Use with care, esp in multi-process models with identical streams + """ + def sample(self, n: int, cat_weights: nparray[np.float64]) -> nparray[np.int64]: + """ + Returns an array of length n containing randomly sampled categorical values, weighted according to cat_weights + """ + def seed(self) -> int: + """ + Returns the seed used to initialise the random stream + """ + def state(self) -> int: + """ + Returns a hash of the internal state of the generator. Avoids the extra complexity of tranmitting variable-length strings over MPI. + """ + @overload + def stopping(self, lambda_: float, n: int) -> nparray[np.float64]: + """ + Returns an array of stopping times (with hazard rate lambda) of length n + + + Returns an array of stopping times (with hazard rate lambda[i]) for each element in lambda + """ + @overload + def stopping(self, lambda_: nparray[np.float64]) -> nparray[np.float64]: ... + def ustream(self, n: int) -> nparray[np.float64]: + """ + Returns an array of uniform random [0,1) variates of length n + """ + pass +class NoTimeline(Timeline): + """ + An arbitrary one step timeline, for continuous-time models with no explicit (discrete) timeline + """ + def __init__(self) -> None: + """ + Constructs an arbitrary one step timeline, where the start and end times are undefined and there is a single step of size zero. Useful for continuous-time models + """ + def __repr__(self) -> str: + """ + Prints a human-readable representation of the timeline + """ + def at_end(self) -> bool: + """ + Returns True if the current step is the end of the timeline + """ + def dt(self) -> float: + """ + Returns the step size size of the timeline + """ + def end(self) -> object: + """ + Returns the time of the end of the timeline + """ + def index(self) -> int: + """ + Returns the index of the current step in the timeline + """ + def nsteps(self) -> int: + """ + Returns the number of steps in the timeline (or -1 if open-ended) + """ + def start(self) -> object: + """ + Returns the time of the start of the timeline + """ + def time(self) -> object: + """ + Returns the time of the current step in the timeline + """ + pass +class NumericTimeline(Timeline): + """ + An custom non-calendar timeline where the user explicitly specifies the time points, which must be monotonically increasing. + """ + def __init__(self, times: list[float]) -> None: + """ + Constructs a timeline from an array of time points. + """ + def __repr__(self) -> str: + """ + Prints a human-readable representation of the timeline + """ + def at_end(self) -> bool: + """ + Returns True if the current step is the end of the timeline + """ + def dt(self) -> float: + """ + Returns the step size size of the timeline + """ + def end(self) -> object: + """ + Returns the time of the end of the timeline + """ + def index(self) -> int: + """ + Returns the index of the current step in the timeline + """ + def nsteps(self) -> int: + """ + Returns the number of steps in the timeline (or -1 if open-ended) + """ + def start(self) -> object: + """ + Returns the time of the start of the timeline + """ + def time(self) -> object: + """ + Returns the time of the current step in the timeline + """ + pass +class CalendarTimeline(Timeline): + """ + A calendar-based timeline + """ + @overload + def __init__(self, start: datetime.datetime, end: datetime.datetime, step: int, unit: str) -> None: + """ + Constructs a calendar-based timeline, given start and end dates, an increment specified as a multiple of days, months or years + + + 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 + """ + @overload + def __init__(self, start: datetime.datetime, step: int, unit: str) -> None: ... + def __repr__(self) -> str: + """ + Prints a human-readable representation of the timeline + """ + def at_end(self) -> bool: + """ + Returns True if the current step is the end of the timeline + """ + def dt(self) -> float: + """ + Returns the step size size of the timeline + """ + def end(self) -> object: + """ + Returns the time of the end of the timeline + """ + def index(self) -> int: + """ + Returns the index of the current step in the timeline + """ + def nsteps(self) -> int: + """ + Returns the number of steps in the timeline (or -1 if open-ended) + """ + def start(self) -> object: + """ + Returns the time of the start of the timeline + """ + def time(self) -> object: + """ + Returns the time of the current step in the timeline + """ + pass +def checked(checked: bool = True) -> None: + """ + Sets the checked flag, which determines whether the model runs checks during execution + """ +def log(obj: object) -> None: + """ + The logging function. Prints obj to the console, annotated with process information + """ +def run(model: object) -> bool: + """ + Runs the model. If the model has previously run it will resume from the point at which it was given the "halt" instruction. This is useful + for external processing of model data, and/or feedback from external sources. If the model has already reached the end of the timeline, this + function will have no effect. To re-run the model from the start, you must construct a new model object. + Returns: + True if model succeeded, False otherwise + """ +def verbose(verbose: bool = True) -> None: + """ + Sets the verbose flag, which toggles detailed runtime logs + """ diff --git a/neworder/df.pyi b/neworder/df.pyi new file mode 100644 index 00000000..4cb8082b --- /dev/null +++ b/neworder/df.pyi @@ -0,0 +1,37 @@ +""" + Submodule for operations involving direct manipulation of pandas dataframes +""" +from __future__ import annotations +from typing import TypeVar +import neworder +import numpy as np +# _Shape = typing.Tuple[int, ...] + +T = TypeVar("T") +nparray = np.ndarray[T, np.dtype[T]] + +__all__ = [ + "testfunc", + "transition", + "unique_index" +] + + +def testfunc(model: neworder.Model, df: object, colname: str) -> None: + """ + Test function for direct dataframe manipulation. Results may vary. Do not use. + """ +def transition(model: neworder.Model, categories: nparray[np.int64], transition_matrix: nparray[np.float64], df: object, colname: str) -> None: + """ + Randomly changes categorical data in a dataframe, according to supplied transition probabilities. + Args: + model: The model (for access to the MonteCarlo engine). + categories: The set of possible categories + transition_matrix: The probabilities of transitions between categories + df: The dataframe, which is modified in-place + colname: The name of the column in the dataframe + """ +def unique_index(n: int) -> nparray[np.int64]: + """ + Generates an array of n unique values, even across multiple processes, that can be used to unambiguously index multiple dataframes. + """ diff --git a/neworder/mpi.pyi b/neworder/mpi.pyi new file mode 100644 index 00000000..1dca981b --- /dev/null +++ b/neworder/mpi.pyi @@ -0,0 +1,19 @@ +""" + Submodule for basic MPI environment discovery +""" +from __future__ import annotations + +__all__ = [ + "rank", + "size" +] + + +def rank() -> int: + """ + Returns the MPI rank of the process + """ +def size() -> int: + """ + Returns the MPI size (no. of processes) of the run + """ diff --git a/neworder/py.typed b/neworder/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/neworder/stats.pyi b/neworder/stats.pyi new file mode 100644 index 00000000..eef2b8aa --- /dev/null +++ b/neworder/stats.pyi @@ -0,0 +1,56 @@ +""" + Submodule for statistical functions +""" +from __future__ import annotations +from typing import overload, TypeVar +import numpy as np +#_Shape = typing.Tuple[int, ...] + +__all__ = [ + "logistic", + "logit" +] + +T = TypeVar("T") +nparray = np.ndarray[T, np.dtype[T]] + +@overload +def logistic(x: nparray[np.float64]) -> nparray[np.float64]: + """ + Computes the logistic function on the supplied values. + Args: + x: The input values. + k: The growth rate + x0: the midpoint location + Returns: + The function values + + + Computes the logistic function with x0=0 on the supplied values. + Args: + x: The input values. + k: The growth rate + Returns: + The function values + + + Computes the logistic function with k=1 and x0=0 on the supplied values. + Args: + x: The input values. + Returns: + The function values + """ +@overload +def logistic(x: nparray[np.float64], k: float) -> nparray[np.float64]: + pass +@overload +def logistic(x: nparray[np.float64], x0: float, k: float) -> nparray[np.float64]: + pass +def logit(x: nparray[np.float64]) -> nparray[np.float64]: + """ + Computes the logit function on the supplied values. + Args: + x: The input probability values in (0,1). + Returns: + The function values (log-odds) + """ diff --git a/neworder/time.pyi b/neworder/time.pyi new file mode 100644 index 00000000..e6c78cf9 --- /dev/null +++ b/neworder/time.pyi @@ -0,0 +1,44 @@ +""" + Temporal values and comparison +""" +from __future__ import annotations +from typing import overload, TypeVar +import numpy as np + +T = TypeVar("T") +nparray = np.ndarray[T, np.dtype[T]] + + +__all__ = [ + "distant_past", + "far_future", + "isnever", + "never" +] + + +def distant_past() -> float: + """ + Returns a value that compares less than any other value but itself and "never" + """ +def far_future() -> float: + """ + Returns a value that compares greater than any other value but itself and "never" + """ +@overload +def isnever(t: float) -> bool: + """ + Returns whether the value of t corresponds to "never". As "never" is implemented as a floating-point NaN, + direct comparison will always fail, since NaN != NaN. + + + Returns an array of booleans corresponding to whether the element of an array correspond to "never". As "never" is + implemented as a floating-point NaN, direct comparison will always fails, since NaN != NaN. + """ +@overload +def isnever(t: nparray[np.float64]) -> nparray[np.bool8]: + pass +def never() -> float: + """ + Returns a value that compares unequal to any value, including but itself. + """ diff --git a/scripts/code_doc.sh b/scripts/code_doc.sh index 80e16722..b2c7045c 100755 --- a/scripts/code_doc.sh +++ b/scripts/code_doc.sh @@ -4,9 +4,6 @@ VERSION=$(grep __version__ neworder/__init__.py | cut -d' ' -d'=' -d'"' - -f2) sed -e "s/VERSION/${VERSION}/g" docs/examples/src.md_template > docs/examples/src.md -# generate api doc -> apidoc.md -python scripts/docstr2md.py - # zip example code into docs folder find ./examples -type d -name __pycache__ -o -name output > excluded tar zcfv docs/examples/neworder-${VERSION}-examples-src.tgz -X excluded ./examples diff --git a/scripts/docstr2md.py b/scripts/docstr2md.py index ac2e298d..e1522cd1 100644 --- a/scripts/docstr2md.py +++ b/scripts/docstr2md.py @@ -25,6 +25,8 @@ def badge(t): h = "" return "![%s](https://img.shields.io/badge/%s-%s-%s)" % (t, h, t, colour[t]) +raise RuntimeError("DEPRECATED") + def format_overloads(lines): for i, l in enumerate(lines): diff --git a/setup.py b/setup.py index 96b27c81..e21b6a1b 100755 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ def list_files(dirs, exts, exclude=[]): long_description=readme(), long_description_content_type="text/markdown", packages=["neworder"], + package_data={"neworder": ["py.typed", "*.pyi"]}, ext_modules=ext_modules, install_requires=['numpy>=1.19.1', 'pandas>=1.0.5', 'scipy'], setup_requires=['pybind11>=2.5.0', 'pytest-runner'], diff --git a/src/Module_docstr.cpp b/src/Module_docstr.cpp index 51488946..56c50fbb 100644 --- a/src/Module_docstr.cpp +++ b/src/Module_docstr.cpp @@ -283,7 +283,7 @@ const char* time_far_future_docstr = R"docstr( )docstr"; const char* time_never_docstr = R"docstr( - Returns a value that compares unequal to any value, including but itself. + Returns a value that compares unequal to any value, including itself. )docstr"; const char* time_isnever_docstr = R"docstr( diff --git a/test/benchmark.py b/test/benchmark.py index 3257dd1e..e698f068 100644 --- a/test/benchmark.py +++ b/test/benchmark.py @@ -1,9 +1,8 @@ import time import numpy as np -import pandas as pd +import pandas as pd # type: ignore import neworder as no -from math import sqrt no.verbose() @@ -18,16 +17,16 @@ [0.1, 0.1, 0.1, 0.1, 0.5, 0.1 ], [0., 0., 0., 0., 0.2, 0.8 ]]) -c = [-1, 1, 2, 3, 4, 5] +c = np.array([-1, 1, 2, 3, 4, 5]) -def get_data(): +def get_data() -> pd.DataFrame: hh = pd.read_csv(INITIAL_POPULATION)#, nrows=100) for i in range(8): hh = hh.append(hh, ignore_index=True) return hh -def interp(cumprob, x): +def interp(cumprob: np.ndarray[np.float64, np.dtype[np.float64]], x: float) -> int: lbound = 0 while lbound < len(cumprob) - 1: if cumprob[lbound] > x: @@ -35,10 +34,10 @@ def interp(cumprob, x): lbound += 1 return lbound -def sample(u, tc, c): +def sample(u: float, tc: np.ndarray[np.float64, np.dtype[np.float64]], c: np.ndarray[np.float64, np.dtype[np.float64]]) -> float: return c[interp(tc, u)] -def transition(model, c, t, df, colname): +def transition(c: np.ndarray[np.float64, np.dtype[np.float64]], t: np.ndarray[np.float64, np.dtype[np.float64]], df: pd.DataFrame, colname: str) -> None: #u = m.mc.ustream(len(df)) tc = np.cumsum(t, axis=1) @@ -51,13 +50,13 @@ def transition(model, c, t, df, colname): df[colname] = df[colname].apply(lambda current: sample(m.mc.ustream(1), tc[lookup[current]], c)) -def python_impl(m, df): +def python_impl(m: no.Model, df: pd.DataFrame) -> tuple[int, float, pd.Series]: start = time.time() - transition(m, c, t, df, "LC4408_C_AHTHUK11") + transition(c, t, df, "LC4408_C_AHTHUK11") return len(df), time.time() - start, df.LC4408_C_AHTHUK11.values -def cpp_impl(m, df): +def cpp_impl(m: no.Model, df: pd.DataFrame) -> tuple[int, float, pd.Series]: start = time.time() no.df.transition(m, c, t, df, "LC4408_C_AHTHUK11") diff --git a/test/test_domain.py b/test/test_domain.py index c3ac6ea1..b2202504 100644 --- a/test/test_domain.py +++ b/test/test_domain.py @@ -1,7 +1,7 @@ import numpy as np import neworder as no -import pandas as pd +import pandas as pd # type: ignore from utils import assert_throws diff --git a/test/utils.py b/test/utils.py index 645d3de7..86b3701e 100644 --- a/test/utils.py +++ b/test/utils.py @@ -2,7 +2,7 @@ # test utils from typing import Any, Callable, Type -def assert_throws(e: Type[Exception], f: Callable, *args: Any, **kwargs: Any): +def assert_throws(e: Type[Exception], f: Callable, *args: Any, **kwargs: Any) -> None: try: f(*args, **kwargs) except e: