rangekeeper 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rangekeeper/graph.py ADDED
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import uuid
5
+ import enum
6
+ from typing import List, Dict
7
+
8
+ import networkx as nx
9
+ from pint import Quantity
10
+ from py_linq import Enumerable
11
+
12
+ from . import measure
13
+
14
+
15
+ #
16
+ # class Type(enum.Enum):
17
+ # name: str
18
+ # parent: Type
19
+ # children: [Type]
20
+ #
21
+ # def __init__(
22
+ # self,
23
+ # name: str,
24
+ # parent: Type = None,
25
+ # children: List[Type] = None):
26
+ # super().__init__()
27
+ # self.name
28
+ # if parent is not None:
29
+ # self.set_parent(parent)
30
+ # if children is not None:
31
+ # self.set_children(children)
32
+ # else:
33
+ # self.children = []
34
+ #
35
+ # def __str__(self):
36
+ # result = self.name
37
+ # try:
38
+ # result = self.parent.__str__() + '.' + result
39
+ # except AttributeError:
40
+ # pass
41
+ # return result
42
+ #
43
+ # def set_parent(
44
+ # self,
45
+ # parent: Type):
46
+ # try:
47
+ # self.parent.children.remove(self)
48
+ # except AttributeError:
49
+ # pass
50
+ # self.parent = parent
51
+ # parent.children.append(self)
52
+ #
53
+ # def set_children(
54
+ # self,
55
+ # children: List[Type]):
56
+ # for child in children:
57
+ # child.set_parent(self)
58
+ #
59
+
60
+ class Event:
61
+ name: str
62
+ type: str
63
+
64
+
65
+ class Element:
66
+ id: str
67
+ name: str
68
+ type: str
69
+ attributes: dict
70
+ events: List[Event]
71
+ measurements: Dict[measure.Measure, Quantity]
72
+
73
+ def __str__(self):
74
+ return 'Element: ' + self.name + '. Type: ' + self.type
75
+
76
+ def __init__(
77
+ self,
78
+ name: str,
79
+ type: str,
80
+ id: str = None,
81
+ attributes: dict = None,
82
+ events: List[Event] = None,
83
+ measurements: Dict[measure.Measure, Quantity] = None):
84
+ self.name = name
85
+ self.type = type
86
+ self.id = str(uuid.uuid4()) if id is None else id
87
+ self.attributes = {} if attributes is None else attributes
88
+ self.events = [] if events is None else events
89
+ self.measurements = {} if measurements is None else measurements
90
+
91
+ def get_relatives(
92
+ self,
93
+ assembly: Assembly,
94
+ relationship_type: str,
95
+ outgoing: bool = True) -> List[Element]:
96
+
97
+ if outgoing:
98
+ edges = Enumerable(
99
+ assembly.edges(
100
+ nbunch=[self],
101
+ data='type')
102
+ ).where(
103
+ lambda edge: edge[2] == relationship_type)
104
+ return [edge[1] for edge in edges]
105
+
106
+ else:
107
+ edges = Enumerable(
108
+ assembly.in_edges(
109
+ nbunch=[self],
110
+ data='type')
111
+ ).where(
112
+ lambda edge: edge[2] == relationship_type)
113
+ return [edge[0] for edge in edges]
114
+
115
+
116
+ class Assembly(nx.MultiDiGraph, Element):
117
+ elements: Enumerable
118
+ relationships: Enumerable
119
+
120
+ def __init__(
121
+ self,
122
+ name: str,
123
+ type: str,
124
+ elements: List[Element],
125
+ relationships: List[tuple[Element, Element, str]], # A list of tuples of the form [From, To, Type]
126
+ id: str = None,
127
+ attributes: dict = None,
128
+ events: List[Event] = None,
129
+ measurements: Dict[measure.Measure, Quantity] = None):
130
+ super().__init__(
131
+ name=name)
132
+ self.name = name
133
+ self.type = type
134
+ self.id = str(uuid.uuid4()) if id is None else id
135
+ self.attributes = {} if attributes is None else attributes
136
+ self.events = [] if events is None else events
137
+ self.measurements = {} if measurements is None else measurements
138
+
139
+ self.add_nodes_from(elements)
140
+ for relationship in relationships:
141
+ self.add_edge(relationship[0], relationship[1], type=relationship[2])
142
+
143
+ self.elements = Enumerable(self.nodes)
144
+ self.relationships = Enumerable(self.edges)
rangekeeper/measure.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import locale
4
+
5
+ import pint
6
+ #from pint.definitions import UnitDefinition
7
+ #from pint.converters import ScaleConverter
8
+ import moneyed
9
+
10
+
11
+ class Index:
12
+ # Define Fractions:
13
+ registry = pint.UnitRegistry()
14
+ # preprocessors=[lambda s: s.replace('%', ' percent ')]
15
+ # )
16
+ # # registry.define('percent = 0.01 * dimensionless = %')
17
+ # registry.define(UnitDefinition('percent', '%', (), ScaleConverter(1 / 100.0)))
18
+
19
+ # Add Additional Terms:
20
+ registry.define('squaremeter = 1 m**2 = m2 = sqm')
21
+ registry.define('squarefoot = 1 foot**2 = ft2 = sqft')
22
+
23
+ # Define Domain Units:
24
+ registry.define('zone = [space]')
25
+ registry.define('parking_stall = 1 * zone')
26
+
27
+ # self.registry = registry
28
+
29
+ # # Define Scalar:
30
+ # scalar = Measure(
31
+ # name='Scalar Quantity',
32
+ # definition='Dimensionless or Unitless Quantity',
33
+ # units=pint.UnitRegistry().dimensionless)
34
+
35
+ # @staticmethod
36
+
37
+
38
+ def remove_dimension(
39
+ quantity: pint.Quantity,
40
+ dimension: str,
41
+ registry: pint.UnitRegistry = None) -> pint.Quantity:
42
+ """
43
+ Remove a dimension from the units of a quantity. Specify the dimension as a string wrapped in square brackets (['']).
44
+ """
45
+ if dimension in quantity.dimensionality:
46
+ for unit_str in quantity.units._units:
47
+ units = registry.Unit(unit_str) if registry is not None else pint.Unit(unit_str)
48
+ if units.dimensionality == dimension:
49
+ if quantity.dimensionality[dimension] == -1:
50
+ return quantity * units
51
+ elif quantity.dimensionality[dimension] == 1:
52
+ return quantity / units
53
+ else:
54
+ raise NotImplementedError(
55
+ 'Error: Dimension reduction currently only works for single-order (^1 or ^-1) dimensions.')
56
+ else:
57
+ return quantity
58
+
59
+
60
+ def multiply_units(
61
+ units: [pint.Unit],
62
+ registry: pint.UnitRegistry = None) -> pint.Unit:
63
+ """
64
+ Multiply units together.
65
+ """
66
+ registry = registry if registry is not None else pint.UnitRegistry()
67
+ quantities = [registry.Quantity(1, unit) for unit in units]
68
+ result = 1 * registry.dimensionless
69
+ for quantity in quantities:
70
+ result *= quantity
71
+ return result.units
72
+
73
+
74
+ def register_currency(
75
+ registry: pint.UnitRegistry):
76
+ # if unit_registry is None:
77
+ # unit_registry = pint.UnitRegistry()
78
+
79
+ currency = moneyed.Currency(code=locale.localeconv()['int_curr_symbol'].strip())
80
+
81
+ if '[currency]' not in registry._dimensions:
82
+ registry.define('money = [currency]')
83
+ registry.define(
84
+ '{0} = nan money = {0} = {1}'.format(
85
+ ''.join(currency.code.split()),
86
+ ''.join(currency.name.split())))
87
+ else:
88
+ registry.define(
89
+ '{0} = nan money = {0} = {1}'.format(
90
+ ''.join(currency.code.split()),
91
+ ''.join(currency.name.split())))
92
+
93
+ return Measure(
94
+ name=currency.name,
95
+ definition='Currency of {0}'.format(currency.countries),
96
+ units=registry[currency.code].units)
97
+
98
+
99
+ #
100
+ # class Quantity(pint.Quantity):
101
+ # def __init__(
102
+ # self,
103
+ # value,
104
+ # units: pint.Unit
105
+ # ):
106
+ # """
107
+ # value : str, pint.Quantity or any numeric type. Value of the physical quantity to be created.
108
+ # units :
109
+ # """
110
+ # super.__init__()
111
+
112
+
113
+ class Measure:
114
+ name: str
115
+ definition: str
116
+ units: pint.Unit
117
+
118
+ def __init__(
119
+ self,
120
+ name: str,
121
+ units: pint.Unit,
122
+ definition: str = None):
123
+ self.name = name
124
+ self.units = units
125
+ self.definition = definition
126
+
127
+ def __str__(self):
128
+ return 'Rangekeeper Measure: "{0}". {1}. Units: {2}'.format(
129
+ self.name,
130
+ self.definition,
131
+ self.units)
132
+
133
+ def __hash__(self):
134
+ return hash((self.name, self.units.__hash__()))
135
+
136
+ def __eq__(self, other):
137
+ return (self.name == other.name and
138
+ self.units == other.units)
@@ -0,0 +1,5 @@
1
+ from . import deterministic as deterministic
2
+ from . import flexible as flexible
3
+ from . import linear as linear
4
+ from . import linear_graph as linear_graph
5
+ from . import probabilistic as probabilistic
@@ -0,0 +1,143 @@
1
+ import pandas as pd
2
+
3
+ import rangekeeper as rk
4
+
5
+ # except:
6
+ # import modules.rangekeeper.distribution
7
+ # from modules.rangekeeper.flux import Flow, Stream
8
+ # from modules.rangekeeper.periodicity import Periodicity
9
+ # from modules.rangekeeper.span import Span
10
+
11
+
12
+ # Base Model:
13
+ class Model:
14
+ def __init__(
15
+ self,
16
+ params: dict):
17
+
18
+ # Phasing:
19
+ self.acquisition_span = rk.span.Span.from_num_periods(
20
+ name='Acquisition',
21
+ date=params['start_date'],
22
+ period_type=rk.periodicity.Type.YEAR,
23
+ num_periods=1)
24
+
25
+ self.operation_span = rk.span.Span.from_num_periods(
26
+ name='Operation',
27
+ date=rk.periodicity.offset_date(
28
+ date=self.acquisition_span.end_date,
29
+ period_type=rk.periodicity.Type.DAY,
30
+ num_periods=1),
31
+ period_type=rk.periodicity.Type.YEAR,
32
+ num_periods=params['num_periods'])
33
+
34
+ self.projection_span = rk.span.Span.from_num_periods(
35
+ name='Projection',
36
+ date=rk.periodicity.offset_date(
37
+ date=self.operation_span.end_date,
38
+ period_type=rk.periodicity.Type.DAY,
39
+ num_periods=1),
40
+ period_type=rk.periodicity.Type.YEAR,
41
+ num_periods=1)
42
+
43
+ self.noi_calc_span = rk.span.Span.merge(
44
+ name='NOI Calculation Span',
45
+ spans=[self.operation_span, self.projection_span])
46
+
47
+ # Cashflows:
48
+ self.escalation = rk.extrapolation.Compounding(rate=params['growth_rate'])
49
+
50
+ # Potential Gross Income
51
+ self.pgi = rk.flux.Flow.from_projection(
52
+ name='Potential Gross Income',
53
+ value=params['initial_pgi'],
54
+ proj=rk.projection.Extrapolation(
55
+ form=self.escalation,
56
+ sequence=self.noi_calc_span.to_index(period_type=params['period_type'])),
57
+ units=params['units'])
58
+
59
+ self.addl_pgi = rk.flux.Flow(
60
+ movements=pd.Series(
61
+ data=range(self.pgi.movements.size),
62
+ index=self.pgi.movements.index,
63
+ dtype=float),
64
+ units=params['units'],
65
+ name='Additional PGI')
66
+
67
+ self.addl_pgi.movements = self.addl_pgi.movements * params['addl_pgi_per_period']
68
+ self.pgi = rk.flux.Flow(
69
+ movements=self.pgi.movements + self.addl_pgi.movements,
70
+ units=params['units'],
71
+ name=self.pgi.name)
72
+
73
+ # Vacancy Allowance
74
+ self.vacancy = rk.flux.Flow.from_periods(
75
+ name='Vacancy Allowance',
76
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
77
+ data=self.pgi.movements * params['vacancy_rate'],
78
+ units=params['units']).invert()
79
+
80
+ # Effective Gross Income:
81
+ self.egi = rk.flux.Stream(
82
+ name='Effective Gross Income',
83
+ flows=[self.pgi, self.vacancy],
84
+ period_type=params['period_type'])
85
+
86
+ # Operating Expenses:
87
+ self.opex = rk.flux.Flow.from_periods(
88
+ name='Operating Expenses',
89
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
90
+ data=self.pgi.movements * params['opex_pgi_ratio'],
91
+ units=params['units']).invert()
92
+
93
+ # Net Operating Income:
94
+ self.noi = rk.flux.Stream(
95
+ name='Net Operating Income',
96
+ flows=[self.egi.sum('Effective Gross Income'), self.opex],
97
+ period_type=params['period_type'])
98
+
99
+ # Capital Expenses:
100
+ self.capex = rk.flux.Flow.from_periods(
101
+ name='Capital Expenditures',
102
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
103
+ data=self.pgi.movements * params['capex_pgi_ratio'],
104
+ units=params['units']).invert()
105
+
106
+ # Net Cashflows:
107
+ self.ncf = rk.flux.Stream(
108
+ name='Net Cashflows',
109
+ flows=[self.noi.sum(), self.capex],
110
+ period_type=params['period_type'])
111
+
112
+ # Disposition
113
+ # We require each next period's NCF as the numerator:
114
+ sale_values = list(self.ncf.sum().movements.iloc[1:] / params['cap_rate'])
115
+ self.disposition = rk.flux.Flow.from_periods(
116
+ name='Disposition',
117
+ # We no longer need the noi_calc_span:
118
+ index=self.operation_span.to_index(period_type=params['period_type']),
119
+ data=sale_values,
120
+ units=params['units'])
121
+
122
+ # Calculate the Present Value of the NCFs:
123
+ self.pv_ncf = self.ncf.sum().pv(
124
+ period_type=params['period_type'],
125
+ discount_rate=params['discount_rate'])
126
+
127
+ # Calculate the Present Value of Reversion CFs:
128
+ self.pv_disposition = self.disposition.pv(
129
+ period_type=params['period_type'],
130
+ discount_rate=params['discount_rate'])
131
+
132
+ # Add Cumulative Sum of Discounted Net Cashflows to each period's Discounted Disposition
133
+ pv_ncf_cumsum = rk.flux.Flow(
134
+ movements=self.pv_ncf.movements.cumsum(),
135
+ name='Discounted Net Cashflow Cumulative Sums',
136
+ units=params['units'])
137
+ self.pv_ncf_agg = rk.flux.Stream(
138
+ name='Discounted Net Cashflow Sums',
139
+ flows=[pv_ncf_cumsum, self.pv_disposition],
140
+ period_type=params['period_type'])
141
+
142
+ self.pv_sums = self.pv_ncf_agg.sum()
143
+ self.pv_sums.movements = self.pv_sums.movements[:-1]
@@ -0,0 +1,177 @@
1
+ import pandas as pd
2
+
3
+ import rangekeeper as rk
4
+
5
+
6
+ # Base Model:
7
+ class Model:
8
+ def __init__(
9
+ self,
10
+ params: dict):
11
+
12
+ # Phasing:
13
+ self.acquisition_span = rk.span.Span.from_num_periods(
14
+ name='Acquisition',
15
+ date=params['start_date'],
16
+ period_type=rk.periodicity.Type.YEAR,
17
+ num_periods=1)
18
+
19
+ self.operation_span = rk.span.Span.from_num_periods(
20
+ name='Operation',
21
+ date=rk.periodicity.offset_date(
22
+ date=self.acquisition_span.end_date,
23
+ period_type=rk.periodicity.Type.DAY,
24
+ num_periods=1),
25
+ period_type=rk.periodicity.Type.YEAR,
26
+ num_periods=params['num_periods'])
27
+
28
+ self.disposition_span = rk.span.Span.from_num_periods(
29
+ name='Disposition',
30
+ date=rk.periodicity.offset_date(
31
+ date=self.acquisition_span.start_date,
32
+ period_type=rk.periodicity.Type.YEAR,
33
+ num_periods=params['num_periods']),
34
+ period_type=rk.periodicity.Type.YEAR,
35
+ num_periods=1)
36
+
37
+ self.projection_span = rk.span.Span.from_num_periods(
38
+ name='Projection',
39
+ date=rk.periodicity.offset_date(
40
+ date=self.operation_span.end_date,
41
+ period_type=rk.periodicity.Type.DAY,
42
+ num_periods=1),
43
+ period_type=rk.periodicity.Type.YEAR,
44
+ num_periods=1)
45
+
46
+ self.noi_calc_span = rk.span.Span.merge(
47
+ name='NOI Calculation Span',
48
+ spans=[self.operation_span, self.projection_span])
49
+
50
+ # Cashflows:
51
+ self.escalation = rk.extrapolation.Compounding(rate=params['growth_rate'])
52
+
53
+ # Potential Gross Income
54
+ self.pgi = rk.flux.Flow.from_projection(
55
+ name='Potential Gross Income',
56
+ value=params['initial_pgi'],
57
+ proj=rk.projection.Extrapolation(
58
+ form=self.escalation,
59
+ sequence=self.noi_calc_span.to_index(period_type=params['period_type'])),
60
+ units=params['units'])
61
+
62
+ factors = params['space_market_dist'].sample(size=self.pgi.movements.size)
63
+ self.pgi_factor = rk.flux.Flow(
64
+ movements=pd.Series(
65
+ data=factors,
66
+ index=self.pgi.movements.index,
67
+ dtype=float),
68
+ units=params['units'],
69
+ name='Space Market Pricing Factors')
70
+
71
+ self.pgi = rk.flux.Flow(
72
+ movements=self.pgi.movements * self.pgi_factor.movements,
73
+ units=params['units'],
74
+ name=self.pgi.name)
75
+
76
+ # Vacancy Allowance
77
+ self.vacancy = rk.flux.Flow.from_periods(
78
+ name='Vacancy Allowance',
79
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
80
+ data=self.pgi.movements * params['vacancy_rate'],
81
+ units=params['units']).invert()
82
+
83
+ # Effective Gross Income:
84
+ self.egi = rk.flux.Stream(
85
+ name='Effective Gross Income',
86
+ flows=[self.pgi, self.vacancy],
87
+ period_type=params['period_type'])
88
+
89
+ # Operating Expenses:
90
+ self.opex = rk.flux.Flow.from_periods(
91
+ name='Operating Expenses',
92
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
93
+ data=self.pgi.movements * params['opex_pgi_ratio'],
94
+ units=params['units']).invert()
95
+
96
+ # Net Operating Income:
97
+ self.noi = rk.flux.Stream(
98
+ name='Net Operating Income',
99
+ flows=[self.egi.sum('Effective Gross Income'), self.opex],
100
+ period_type=params['period_type'])
101
+
102
+ # Capital Expenses:
103
+ self.capex = rk.flux.Flow.from_periods(
104
+ name='Capital Expenditures',
105
+ index=self.noi_calc_span.to_index(period_type=params['period_type']),
106
+ data=self.pgi.movements * params['capex_pgi_ratio'],
107
+ units=params['units']).invert()
108
+
109
+ # Net Cashflows:
110
+ self.ncf = rk.flux.Stream(
111
+ name='Net Cashflows',
112
+ flows=[self.noi.sum(), self.capex],
113
+ period_type=params['period_type'])
114
+
115
+ # Reversion:
116
+ # We no longer need the noi_calc_span:
117
+ operation_periods = self.operation_span.to_index(period_type=params['period_type'])
118
+ self.cap_rates = params['asset_market_dist'].sample(size=operation_periods.size)
119
+ self.sale_values = rk.flux.Flow.from_periods(
120
+ name='Sale Values',
121
+ # We no longer need the noi_calc_span:
122
+ index=operation_periods,
123
+ data=list(self.ncf.sum().movements[
124
+ :self.operation_span.end_date] / self.cap_rates),
125
+ units=params['units'])
126
+
127
+ # Flexibility Rules:
128
+ disposition_flags = []
129
+ for i in range(self.sale_values.movements.size):
130
+ flag = False
131
+ if i > 1:
132
+ if not any(disposition_flags):
133
+ if self.pgi_factor.movements[i] > 1.2:
134
+ self.disposition_date = self.sale_values.movements.index[i]
135
+ flag = True
136
+ disposition_flags.append(flag)
137
+
138
+ self.disposition = rk.flux.Flow(
139
+ name="Disposition",
140
+ movements=pd.Series(
141
+ data=self.sale_values.movements * disposition_flags,
142
+ index=self.sale_values.movements.index),
143
+ units=params['units'])
144
+
145
+ # Calculate the Present Value of the NCFs:
146
+ self.pv_ncf = self.ncf.sum().pv(
147
+ period_type=params['period_type'],
148
+ discount_rate=params['discount_rate'])
149
+
150
+ # Calculate the Present Value of Disposition CFs:
151
+ self.pv_disposition = self.disposition.pv(
152
+ period_type=params['period_type'],
153
+ discount_rate=params['discount_rate'])
154
+
155
+ self.pv_ncf_agg = rk.flux.Stream(
156
+ name='Discounted Net Cashflow Sums',
157
+ flows=[self.pv_ncf, self.pv_disposition],
158
+ period_type=params['period_type'])
159
+
160
+ self.pv_sums = self.pv_ncf_agg.sum()
161
+
162
+ self.pv_sums.movements = self.pv_sums.movements[:self.disposition_date]
163
+
164
+ self.acquisition = rk.flux.Flow.from_periods(
165
+ index=self.acquisition_span.to_index(rk.periodicity.Type.YEAR),
166
+ data=[-abs(params['acquisition_price'])],
167
+ units=params['units'],
168
+ name='Acquisition Price')
169
+
170
+ self.investment_cashflows = rk.flux.Stream(
171
+ name='Investment Cashflows',
172
+ flows=[self.ncf.sum(), self.disposition, self.acquisition],
173
+ period_type=params['period_type'])
174
+
175
+ self.investment_cashflows.frame = self.investment_cashflows.frame[:self.disposition_date]
176
+ self.irr = self.investment_cashflows.sum().xirr()
177
+ self.npv = self.investment_cashflows.sum().xnpv(params['discount_rate'])