Skip to content

Commit

Permalink
Merge pull request #44 from kartoza/feat-add-task-code-version
Browse files Browse the repository at this point in the history
Feat add task code version
  • Loading branch information
danangmassandy committed Jun 7, 2024
2 parents 52379d9 + 1f73499 commit 1fbae62
Show file tree
Hide file tree
Showing 12 changed files with 794 additions and 40 deletions.
3 changes: 2 additions & 1 deletion django_project/core/settings/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db import connection
from boto3.s3.transfer import TransferConfig
from .contrib import * # noqa
from .utils import code_release_version
from .utils import code_release_version, code_commit_release_version

ALLOWED_HOSTS = ['*']
ADMINS = (
Expand Down Expand Up @@ -44,6 +44,7 @@
)

CODE_RELEASE_VERSION = code_release_version()
CODE_COMMIT_HASH = code_commit_release_version()

# s3
# TODO: set CacheControl in object_parameters+endpoint_url
Expand Down
10 changes: 10 additions & 0 deletions django_project/core/settings/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ def code_release_version():
return '0.0.1'


def code_commit_release_version():
""" Read code commit release version from file."""
version = absolute_path('version', 'commit.txt')
if os.path.exists(version):
commit = (open(version, 'rb').read()).decode("utf-8")
if commit:
return commit
return 'no-info'


class UUIDEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, uuid.UUID):
Expand Down
22 changes: 22 additions & 0 deletions django_project/cplus/definitions/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
NCS_PATHWAY_SEGMENT = "ncs_pathways"
NCS_CARBON_SEGMENT = "ncs_carbon"
PRIORITY_LAYERS_SEGMENT = "priority_layers"
NPV_PRIORITY_LAYERS_SEGMENT = "npv"

# Naming for outputs sub-folder relative to base directory
OUTPUTS_SEGMENT = "outputs"
Expand All @@ -31,10 +32,31 @@
STYLE_ATTRIBUTE = "style"
USER_DEFINED_ATTRIBUTE = "user_defined"
UUID_ATTRIBUTE = "uuid"
YEARS_ATTRIBUTE = "years"
DISCOUNT_ATTRIBUTE = "discount"
ABSOLUTE_NPV_ATTRIBUTE = "absolute_npv"
NORMALIZED_NPV_ATTRIBUTE = "normalized_npv"
YEARLY_RATES_ATTRIBUTE = "yearly_rates"
ENABLED_ATTRIBUTE = "enabled"
MIN_VALUE_ATTRIBUTE = "minimum_value"
MAX_VALUE_ATTRIBUTE = "maximum_value"
COMPUTED_ATTRIBUTE = "use_computed"
NPV_MAPPINGS_ATTRIBUTE = "mappings"
REMOVE_EXISTING_ATTRIBUTE = "remove_existing"

ACTIVITY_IDENTIFIER_PROPERTY = "activity_identifier"
NPV_COLLECTION_PROPERTY = "npv_collection"

# Option / settings keys
CPLUS_OPTIONS_KEY = "cplus_main"
LOG_OPTIONS_KEY = "cplus_log"
REPORTS_OPTIONS_KEY = "cplus_report"

# Headers for financial NPV computation
YEAR_HEADER = "Year"
TOTAL_PROJECTED_COSTS_HEADER = "Projected Total Costs/ha (US$)"
TOTAL_PROJECTED_REVENUES_HEADER = "Projected Total Revenues/ha (US$)"
DISCOUNTED_VALUE_HEADER = "Discounted Value (US$)"
MAX_YEARS = 99

NO_DATA_VALUE = -9999
7 changes: 7 additions & 0 deletions django_project/cplus/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ def __eq__(self, other) -> bool:
"LayerModelComponentType", bound=LayerModelComponent
)

class PriorityLayerType(IntEnum):
"""Type of priority weighting layer."""

DEFAULT = 0
NPV = 1


@dataclasses.dataclass
class PriorityLayer(BaseModelComponent):
Expand All @@ -213,6 +219,7 @@ class PriorityLayer(BaseModelComponent):
groups: list
selected: bool = False
path: str = ""
type: PriorityLayerType = PriorityLayerType.DEFAULT


@dataclasses.dataclass
Expand Down
167 changes: 167 additions & 0 deletions django_project/cplus/models/financial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-

""" Data models for the financial elements of the tool."""

import dataclasses
from enum import IntEnum
import typing

from .base import Activity


@dataclasses.dataclass
class NpvParameters:
"""Parameters for computing an activity's NPV."""

years: int
discount: float
absolute_npv: float = 0.0
normalized_npv: float = 0.0
# Each tuple contains 3 elements i.e. revenue, costs and discount rates
yearly_rates: typing.List[tuple] = dataclasses.field(default_factory=list)


@dataclasses.dataclass
class ActivityNpv:
"""Mapping of the NPV parameters to the corresponding Activity model."""

params: NpvParameters
enabled: bool
activity: typing.Optional[Activity]

@property
def activity_id(self) -> str:
"""Gets the identifier of the activity model.
:returns: The unique identifier of the activity model else an
empty string if no activity has been set.
"""
if not self.activity:
return ""

return str(self.activity.uuid)

@property
def base_name(self) -> str:
"""Returns a proposed name for the activity NPV.
An empty string will be return id the `activity` attribute
is not set.
:returns: Proposed base name for the activity NPV.
:rtype: str
"""
if self.activity is None:
return ""

return f"{self.activity.name} NPV Norm"


@dataclasses.dataclass
class ActivityNpvCollection:
"""Collection for all ActivityNpvMapping configurations that have been
specified by the user.
"""

minimum_value: float
maximum_value: float
use_computed: bool = True
remove_existing: bool = False
mappings: typing.List[ActivityNpv] = dataclasses.field(default_factory=list)

def activity_npv(self, activity_identifier: str) -> typing.Optional[ActivityNpv]:
"""Gets the mapping of an activity's NPV mapping if defined.
:param activity_identifier: Unique identifier of an activity whose
NPV mapping is to be retrieved.
:type activity_identifier: str
:returns: The activity's NPV mapping else None if not found.
:rtype: ActivityNpv
"""
matching_mapping = [
activity_npv
for activity_npv in self.mappings
if activity_npv.activity_id == activity_identifier
]

return None if len(matching_mapping) == 0 else matching_mapping[0]

def update_computed_normalization_range(self) -> bool:
"""Update the minimum and maximum normalization values
based on the absolute values of the existing ActivityNpv
objects.
Values for disabled activity NPVs will be excluded from
the computation.
:returns: True if the min/max values were updated else False if
there are no mappings or valid absolute NPV values defined.
"""
if len(self.mappings) == 0:
return False

valid_npv_values = [
activity_npv.params.absolute_npv
for activity_npv in self.mappings
if activity_npv.params.absolute_npv is not None and activity_npv.enabled
]

if len(valid_npv_values) == 0:
return False

self.minimum_value = min(valid_npv_values)
self.maximum_value = max(valid_npv_values)

return True

def normalize_npvs(self) -> bool:
"""Normalize the NPV values of the activities using the specified
normalization range.
If the absolute NPV values are less than or greater than the
normalization range, then they will be truncated to 0.0 and 1.0
respectively. To avoid such a situation from occurring, it is recommended
to make sure that the ranges are synchronized using the latest absolute
NPV values hence call `update_computed_normalization_range` before
normalizing the NPVs.
:returns: True if the NPVs were successfully normalized else False due
to various reasons such as if the minimum value is greater than the
maximum value or if the min/max values are the same.
"""
if self.minimum_value > self.maximum_value:
return False

norm_range = float(self.maximum_value - self.minimum_value)

if norm_range == 0.0:
return False

for activity_npv in self.mappings:
absolute_npv = activity_npv.params.absolute_npv
if not absolute_npv:
continue

if absolute_npv <= self.minimum_value:
normalized_npv = 0.0
elif absolute_npv >= self.maximum_value:
normalized_npv = 1.0
else:
normalized_npv = (absolute_npv - self.minimum_value) / norm_range

activity_npv.params.normalized_npv = normalized_npv

return True


@dataclasses.dataclass
class ActivityNpvPwl:
"""Convenience class that contains parameters for creating
a PWL raster layer.
"""

npv: ActivityNpv
extent: typing.List[float]
crs: str
pixel_size: float
Loading

0 comments on commit 1fbae62

Please sign in to comment.