# pragma: no cover
"""
This module defines a bounding box type and implements a constraint solver
that can position multiple bounding boxes s/t they do not overlap
(in addition to other constraints).

main() function used for testing purposes. Primary function made available to outside callers
is run_model()
"""
from itertools import combinations
from typing import Mapping, AbstractSet, Optional, List, Iterable, Tuple, DefaultDict
from attr import attrs, attrib
from collections import defaultdict
from math import isnan, pi

import numpy as np
import torch
import torch.nn as nn
from torch.nn import Parameter
import torch.optim as optim
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torch.nn import PairwiseDistance

from panda3d.core import LPoint3f  # pylint: disable=no-name-in-module

from immutablecollections import immutabledict, immutableset, ImmutableDict, ImmutableSet
from vistautils.preconditions import check_arg

from adam.axes import (
    Axes,
    HorizontalAxisOfObject,
    straight_up,
    # directed,
    symmetric,
    symmetric_vertical,
    FacingAddresseeAxis,
)

from adam.ontology.phase1_spatial_relations import (
    PROXIMAL,
    DISTAL,
    EXTERIOR_BUT_IN_CONTACT,
    Region,
)
from adam.ontology.phase1_spatial_relations import (
    Direction,
    GRAVITATIONAL_UP,
    GRAVITATIONAL_DOWN,
)
from adam.perception import ObjectPerception, GROUND_PERCEPTION

# see https://github.com/pytorch/pytorch/issues/24807 re: pylint issue

ORIGIN = torch.zeros(3, dtype=torch.float)  # pylint: disable=not-callable
# penalty weighting for keeping objects on the ground
GRAVITY_PENALTY = torch.tensor([1], dtype=torch.float)  # pylint: disable=not-callable
# penalty weighting for keeping objects from clipping into the ground
BELOW_GROUND_PENALTY = 2 * GRAVITY_PENALTY
# penalty weighting for keeping objects from colliding with one another
COLLISION_PENALTY = 5 * GRAVITY_PENALTY

LOSS_EPSILON = 1.0e-04

# penalty weighting for adjusting the angle between relatively-positioned objects
ANGLE_PENALTY = torch.tensor([1.5], dtype=torch.float)  # pylint: disable=not-callable
# penalty weighting for adjusting the separation of relatively-positioned objects
DISTANCE_PENALTY = torch.tensor([1], dtype=torch.float)  # pylint: disable=not-callable

# concreteized definitions of the relative distance categories:
PROXIMAL_MIN_DISTANCE = torch.tensor(  # pylint: disable=not-callable
    [1], dtype=torch.float
)
PROXIMAL_MAX_DISTANCE = torch.tensor(  # pylint: disable=not-callable
    [2.5], dtype=torch.float
)

DISTAL_MIN_DISTANCE = torch.tensor([3], dtype=torch.float)  # pylint: disable=not-callable

EXTERIOR_BUT_IN_CONTACT_EPS = torch.tensor(  # pylint: disable=not-callable
    [1e-2], dtype=torch.float
)
PI = torch.tensor([pi], dtype=torch.float)  # pylint: disable=not-callable

def main() -> None:

    top_to_bottom = straight_up("top-to-bottom")
    side_to_side_0 = symmetric("side-to-side-0")
    side_to_side_1 = symmetric("side-to-side-1")

    box = ObjectPerception(
        "box",
        geon=None,
        axes=Axes(
            primary_axis=top_to_bottom,
            orienting_axes=immutableset([side_to_side_0, side_to_side_1]),
        ),
    )

    generating_axis = symmetric_vertical("ball-generating")
    orienting_axis_0 = symmetric("ball-orienting-0")
    orienting_axis_1 = symmetric("ball-orienting-1")

    # ball situated on top of box
    ball = ObjectPerception(
        "ball",
        geon=None,
        axes=Axes(
            primary_axis=generating_axis,
            orienting_axes=immutableset([orienting_axis_0, orienting_axis_1]),
        ),
    )

    in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]] = {
        ball: [Region[ObjectPerception](box, EXTERIOR_BUT_IN_CONTACT, GRAVITATIONAL_UP)]
    }

    # other objects have no particular constraints:

    positioning_model = PositioningModel.for_objects_random_positions(
        object_perceptions=immutableset([ball, box]),
        sub_objects={},
        in_region_relations=in_region_relations,
    )
    # we will start with an aggressive learning rate
    optimizer = optim.SGD(positioning_model.parameters(), lr=1.0)
    # but will decrease it whenever the loss plateaus
    learning_rate_schedule = ReduceLROnPlateau(
        optimizer,
        "min",
        # decrease the rate if the loss hasn't improved in
        # 3 epochs
        patience=3,
    )

    iterations = 100
    for iteration in range(iterations):
        print(f"====== Iteration {iteration} ======")
        positioning_model.dump_object_positions(prefix="\t")

        loss = positioning_model()
        print(f"\tLoss: {loss.item()}")
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

        learning_rate_schedule.step(loss)

    print("========= Final Positions ========")
    positioning_model.dump_object_positions(prefix="\t")

@attrs(frozen=True, auto_attribs=True)
class PositionsMap:
    """Convenience type: list of positions corresponding to objects in a scene."""

    name_to_position: Mapping[str, torch.Tensor]
    name_to_scale: Mapping[str, torch.Tensor]

    def __len__(self) -> int:
        return len(self.name_to_position)

def run_model(
    top_level_objects: ImmutableSet[ObjectPerception],
    sub_objects: Mapping[str, Mapping[str, LPoint3f]],
    in_region_map: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
    object_scales: Mapping[str, Tuple[float, float, float]],
    *,
    frozen_objects: ImmutableSet[str],
    num_iterations: int = 200,
    yield_steps: Optional[int] = None,
    previous_positions: Optional[PositionsMap] = None,
) -> Iterable[PositionsMap]:
    r"""
    Construct a positioning model given a list of objects to position, return their position values.
    The model will return final positions either after the given number of iterations, or if the model
    converges in a position where it is unable to find a gradient to continue.
    Args:
        top_level_objects: set of top-level objects requested to be positioned
        sub_objects: mapping of sub-objects to their parent, and their relative position to that parent
        in_region_map: in-region relations for all top-level objects in this scene
        *num_iterations*: total number of SGD iterations.
        *yield_steps*: If provided, the current positions of all objects will be returned after this many steps
        previous_positions: If provided, attempt to use the positions contained within to initialize the scene

    Returns: PositionsMap: Map of object name -> Tensor (3,) of its position

    """

    if previous_positions:
        positioning_model = PositioningModel.for_scaled_objects_positioned(
            top_level_objects,
            sub_objects,
            in_region_relations=in_region_map,
            scale_map=object_scales,
            positions_map=previous_positions,
            frozen_objects=frozen_objects,
        )
    else:
        positioning_model = PositioningModel.for_scaled_objects_random_positions(
            top_level_objects,
            sub_objects,
            in_region_relations=in_region_map,
            scale_map=object_scales,
        )
    patience = 10
    # we will start with an aggressive learning rate
    optimizer = optim.SGD(positioning_model.parameters(), lr=0.25)
    # but will decrease it whenever the loss plateaus
    learning_rate_schedule = ReduceLROnPlateau(
        optimizer,
        "min",
        # decrease the rate if the loss hasn't improved in
        # 10 epochs
        patience=patience,
    )

    iterations = num_iterations
    quit_patience = patience * 3
    loss_eps = 1e-6
    prev_loss = 0.0
    epochs_without_improvement = 0
    for i in range(iterations):
        # print(f"====== Iteration {i} =======")
        loss = positioning_model()
        # if we lose any substantial gradient, stop the search
        if loss < LOSS_EPSILON:
            break
        if isnan(loss) or prev_loss - loss < loss_eps:
            epochs_without_improvement += 1
        else:
            epochs_without_improvement = 0
        if epochs_without_improvement >= quit_patience:
            break
        prev_loss = loss
        # print(f"\tLoss: {loss.item()} bad: {epochs_without_improvement}")
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

        learning_rate_schedule.step(loss)

        # positioning_model.dump_object_positions(prefix="\t")
        if yield_steps and i % yield_steps == 0:
            yield positioning_model.get_objects_positions()

    # this *looks* redundant, but it helps to ensure that the final positions are rendered
    yield positioning_model.get_objects_positions()
    return positioning_model.get_objects_positions()

@attrs(slots=True, eq=False)
class AxisAlignedBoundingBox:
    """
    Defines a 3D Box that is oriented to world axes.

    This box is defined by a center point of shape (3,),
    and a scale (also of shape (3,)) which defines how a unit cube
    with the given center will be scaled in each dimension to create
    this box.

    For example: a box centered at (0, 0, 0) and with a scale of (1, 1, 1) would have opposite
    corners at (-1, -1, -1) and (1, 1, 1), giving the box a volume of 2(^3)
    """

    center: torch.Tensor = attrib()  # tensor shape: (3,)
    scale: torch.Tensor = attrib()  # tensor shape: (3, 3) - diagonal matrix
    # rotation: torch.Tensor = attrib()
    offset: Optional[torch.Tensor] = attrib()

    def center_distance_from_point(self, point: torch.Tensor) -> torch.Tensor:
        return torch.dist(self.center, point, 2)

    def nearest_center_face_distance_from_point(
        self, point: torch.Tensor
    ) -> torch.Tensor:
        """ returns the distance from the closest face center to the given point

        Args:
            point: tensor (3,) of a coordinate to check distance from this box's face centers
        Returns: tensor (1,)

        """
        centers = self.get_face_centers()
        return torch.min(PairwiseDistance().forward(centers, point.expand(6, 3)))

    def nearest_center_face_point(self, point: torch.Tensor) -> torch.Tensor:
        """
        Returns the closest face center of the box to the given point
        Args:
            point: tensor (3,) x,y,z coordinate: an arbitrary point in 3d space

        Returns: tensor (3,) x,y,z coordinate: the center face of the box closest to the function argument

        """
        face_centers = self.get_face_centers()
        return face_centers[
            torch.argmin(PairwiseDistance().forward(face_centers, point.expand(6, 3)))
        ]

    def z_coordinate_of_lowest_corner(self) -> torch.Tensor:
        """
        Get the position of the lowest corner of the box.

        The third coordinate is interpreted as the height relative to the ground at z=0.
        Returns: (1,) tensor

        """
        # all corners are considered (in case of a rotated box)
        corners = self.get_corners()
        # find the minimum z value
        min_corner_z = torch.min(
            # gather just the z coordinate from the tensor of all corners
            # turning (8,3) into (8,1)
            torch.gather(
                corners,
                1,
                # create a (8, 1) tensor filled with elements corresponding to the index of the Z coordinate
                # these indices are used by torch.gather() to retrieve the correct elements from the corners tensor
                torch.repeat_interleave(
                    torch.tensor([[2]]),  # pylint: disable=not-callable
                    torch.tensor([8]),  # pylint: disable=not-callable
                    dim=0,
                ),
            )
        )
        return min_corner_z - ORIGIN[2]

    @staticmethod
    def create_at_random_position(
        *, min_distance_from_origin: float, max_distance_from_origin: float
    ):
        return AxisAlignedBoundingBox.create_at_random_position_scaled(
            min_distance_from_origin=min_distance_from_origin,
            max_distance_from_origin=max_distance_from_origin,
            object_scale=torch.ones(3),
        )

    @staticmethod
    def create_at_center_point(*, center: np.array):
        return AxisAlignedBoundingBox(
            Parameter(
                torch.tensor(center, dtype=torch.float),  # pylint: disable=not-callable
                requires_grad=True,
            ),
            torch.diag(torch.ones(3)),
            offset=None,
        )

    @staticmethod
    def create_at_center_point_scaled(
        *,
        center: np.array,
        object_scale: torch.Tensor,
        is_parameter: bool,
        offset: Optional[np.array] = None,
    ):
        # if this AABB does not have an offset, it is a top-level object
        if offset is None:
            return AxisAlignedBoundingBox(
                Parameter(
                    torch.tensor(  # pylint: disable=not-callable
                        center, dtype=torch.float
                    ),
                    requires_grad=is_parameter,
                ),
                torch.diag(object_scale),
                offset,
            )
        # otherwise: this is a sub-object. It isn't a parameter and does have an offset from its parent object
        return AxisAlignedBoundingBox(
            torch.tensor(center, dtype=torch.float),  # pylint: disable=not-callable
            torch.diag(object_scale),
            torch.tensor(offset, dtype=torch.float),  # pylint: disable=not-callable
        )

    @staticmethod
    def create_at_random_position_scaled(
        *,
        min_distance_from_origin: float,
        max_distance_from_origin: float,
        object_scale: torch.Tensor,
    ):
        check_arg(min_distance_from_origin > 0.0)
        check_arg(min_distance_from_origin < max_distance_from_origin)
        # we first generate a random point on the unit sphere by
        # generating a random vector in cube...
        center = np.random.randn(3, 1).squeeze()
        # and then normalizing.
        center /= np.linalg.norm(center)

        # then we scale according to the distances above
        scale_factor = np.random.uniform(
            min_distance_from_origin, max_distance_from_origin
        )
        center *= scale_factor
        return AxisAlignedBoundingBox(
            Parameter(
                torch.tensor(center, dtype=torch.float),  # pylint: disable=not-callable
                requires_grad=True,
            ),
            torch.diag(object_scale),
            offset=None,
        )

    def get_corners(self) -> torch.Tensor:
        return self.center.expand(8, 3) + torch.tensor(  # pylint: disable=not-callable
            [
                [-1, -1, -1],
                [1, -1, -1],
                [-1, 1, -1],
                [1, 1, -1],
                [-1, -1, 1],
                [1, -1, 1],
                [-1, 1, 1],
                [1, 1, 1],
            ],
            dtype=torch.float,
        ).matmul(self.scale)
        # see https://github.com/pytorch/pytorch/issues/24807 re: pylint issue

    def get_face_centers(self) -> torch.Tensor:
        """
        Returns the center point of each of the box's 6 faces.
        Returns: tensor (6, 3)
        """
        corners = self.get_corners()
        return torch.stack(  # pylint: disable=not-callable
            [
                torch.div(corners[0] + corners[6], 2),  # left face
                torch.div(corners[0] + corners[5], 2),  # backward face
                torch.div(corners[1] + corners[7], 2),  # right face
                torch.div(corners[2] + corners[7], 2),  # forward face
                torch.div(corners[0] + corners[3], 2),  # bottom face
                torch.div(corners[4] + corners[7], 2),  # top face
            ],
            dim=0,
        )

    def _minus_ones_corner(self) -> torch.Tensor:
        """
        Corner in the direction of the negative x, y, z axes from center
        Returns: Tensor (3,)

        """
        return self.center + torch.tensor(  # pylint: disable=not-callable
            [-1, -1, -1], dtype=torch.float
        ).matmul(self.scale)

    # functions returning normal vectors from three perpendicular faces of the box
    def right_face_normal_vector(self) -> torch.Tensor:
        """
        Normal vector for the right face of the box (toward positive x axis when aligned to world axes)
        Returns: Tensor (3,)
        """
        diff = (
            self.center
            + torch.tensor(  # pylint: disable=not-callable
                [1, -1, -1], dtype=torch.float
            ).matmul(self.scale)
            - self._minus_ones_corner()
        )
        return diff / torch.norm(diff)

    def forward_face_normal_vector(self) -> torch.Tensor:
        """
        Normal vector for the forward face of the box (toward positive y axis when aligned to world axes)
        Returns: Tensor (3,)
        """
        diff = (
            self.center
            + torch.tensor(  # pylint: disable=not-callable
                [-1, 1, -1], dtype=torch.float
            ).matmul(self.scale)
            - self._minus_ones_corner()
        )
        return diff / torch.norm(diff)

    def up_face_normal_vector(self) -> torch.Tensor:
        """
        Normal vector for the up face of the box (toward positive z axis when aligned to world axes)
        Returns: Tensor (3,)
        """
        diff = (
            self.center
            + torch.tensor(  # pylint: disable=not-callable
                [-1, -1, 1], dtype=torch.float
            ).matmul(self.scale)
            - self._minus_ones_corner()
        )
        return diff / torch.norm(diff)

    def face_normal_vectors(self) -> torch.Tensor:
        """
        Stacks the face norms from the right, forward, and up faces of the box
        Returns: Tensor (3,3)

        """
        # in axis-aligned case these are always the same
        return torch.stack(
            [
                self.right_face_normal_vector(),
                self.forward_face_normal_vector(),
                self.up_face_normal_vector(),
            ]
        )

    def corners_onto_axes_projections(self, axes: torch.Tensor) -> torch.Tensor:
        """
        Projects each of 8 corners onto each of three axes.
        Args:
            axes: (3,3) tensor -> the three axes we are projecting points onto

        Returns:
            (3, 8) tensor -> each point projected onto each of three dimensions

        """
        check_arg(axes.shape == (3, 3))
        corners = self.get_corners()
        return axes.matmul(corners.transpose(0, 1))

class PositioningModel(torch.nn.Module):  # type: ignore
    """
    Model that combines multiple constraints on AxisAlignedBoundingBoxes.
    """

    def __init__(
        self,
        object_perception_to_bounding_box: Mapping[
            ObjectPerception, AxisAlignedBoundingBox
        ],
        object_perception_to_excluded_bounding_box: Mapping[
            ObjectPerception, AxisAlignedBoundingBox
        ],
        object_to_sub_object_to_bounding_box: Mapping[
            str, Mapping[str, AxisAlignedBoundingBox]
        ],
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
    ) -> None:
        super().__init__()
        self.object_perception_to_bounding_box = object_perception_to_bounding_box
        self.object_perception_to_excluded_bounding_box = (
            object_perception_to_excluded_bounding_box
        )
        self.object_to_sub_object_to_bounding_box = object_to_sub_object_to_bounding_box
        self.in_region_relations = in_region_relations
        self.object_bounding_boxes = immutableset(
            object_perception_to_bounding_box.values()
        )
        self.excluded_bounding_boxes = immutableset(
            object_perception_to_excluded_bounding_box.values()
        )
        self.included_and_excluded_boxes = self.object_bounding_boxes.union(
            self.excluded_bounding_boxes
        )
        sub_object_to_bounding_box = {}
        for sub_obj_dict in self.object_to_sub_object_to_bounding_box.values():
            for sub_obj_handle, aabb in sub_obj_dict.items():
                sub_object_to_bounding_box[sub_obj_handle] = aabb
        for (
            object_perception,
            bounding_box,
        ) in self.object_perception_to_bounding_box.items():
            # suppress mypy error about supplying a Tensor where it expects a Parameter
            self.register_parameter(  # type: ignore
                object_perception.debug_handle, bounding_box.center
            )

        self.collision_penalty = CollisionPenalty()
        self.below_ground_penalty = BelowGroundPenalty()
        self.weak_gravity_penalty = WeakGravityPenalty(
            object_perception_to_bounding_box, in_region_relations
        )

        self.in_region_penalty = InRegionPenalty(
            object_perception_to_bounding_box,
            object_perception_to_excluded_bounding_box,
            sub_object_to_bounding_box,
            in_region_relations,
        )

    @staticmethod
    def for_objects_random_positions(
        object_perceptions: AbstractSet[ObjectPerception],
        sub_objects: Mapping[str, Mapping[str, LPoint3f]],
        *,
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
    ) -> "PositioningModel":
        objects_to_bounding_boxes: ImmutableDict[
            ObjectPerception, AxisAlignedBoundingBox
        ] = immutabledict(
            (
                object_perception,
                AxisAlignedBoundingBox.create_at_random_position(
                    min_distance_from_origin=10, max_distance_from_origin=20
                ),
            )
            for object_perception in object_perceptions
        )

        sub_object_mapping = PositioningModel._create_sub_objs_to_bounding_boxes(
            sub_objects
        )

        return PositioningModel(
            objects_to_bounding_boxes, {}, sub_object_mapping, in_region_relations
        )

    @staticmethod
    def for_scaled_objects_random_positions(
        object_perceptions: AbstractSet[ObjectPerception],
        sub_objects: Mapping[str, Mapping[str, LPoint3f]],
        *,
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
        scale_map: Mapping[str, Tuple[float, float, float]],
    ) -> "PositioningModel":

        return PositioningModel._scaled_objects_helper(
            object_perceptions=object_perceptions,
            sub_objects=sub_objects,
            in_region_relations=in_region_relations,
            scale_map=scale_map,
            positions_map=None,
        )

    @staticmethod
    def for_scaled_objects_positioned(
        object_perceptions: AbstractSet[ObjectPerception],
        sub_objects: Mapping[str, Mapping[str, LPoint3f]],
        *,
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
        scale_map: Mapping[str, Tuple[float, float, float]],
        positions_map: PositionsMap,
        frozen_objects: ImmutableSet[str],
    ) -> "PositioningModel":

        return PositioningModel._scaled_objects_helper(
            object_perceptions=object_perceptions,
            sub_objects=sub_objects,
            in_region_relations=in_region_relations,
            scale_map=scale_map,
            positions_map=positions_map,
            frozen_objects=frozen_objects,
        )

    @staticmethod
    def _scaled_objects_helper(
        object_perceptions: AbstractSet[ObjectPerception],
        sub_objects: Mapping[str, Mapping[str, LPoint3f]],
        *,
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
        scale_map: Mapping[str, Tuple[float, float, float]],
        positions_map: Optional[PositionsMap],
        frozen_objects: Optional[ImmutableSet[str]] = None,
    ) -> "PositioningModel":

        sub_object_mapping = PositioningModel._create_sub_objs_to_bounding_boxes(
            sub_objects
        )

        dict_items: List[Tuple[ObjectPerception, AxisAlignedBoundingBox]] = []

        excluded_main_objects: List[Tuple[ObjectPerception, AxisAlignedBoundingBox]] = []

        for object_perception in object_perceptions:

            # print(f"Adding {object_perception.debug_handle} to model")

            model_lookup = object_perception.debug_handle.split("_")[0]
            try:
                scale = scale_map[model_lookup]
            except KeyError:
                print(f"couldn't find scale for {object_perception.debug_handle}")
                scale = (1.0, 1.0, 1.0)

            is_parameter = True
            if positions_map:
                # selectively freeze particular objects from positioning model
                if frozen_objects and object_perception.debug_handle in frozen_objects:
                    is_parameter = False
                # print(
                #     f"Creating AABB as parameter: {is_parameter}\ncenter: {positions_map.name_to_position[object_perception.debug_handle]}\nscale: {scale}"
                # )
                bounding_box = AxisAlignedBoundingBox.create_at_center_point_scaled(
                    center=positions_map.name_to_position[object_perception.debug_handle],
                    object_scale=torch.tensor(  # pylint: disable=not-callable
                        [scale[0], scale[1], scale[2]]
                    ),
                    is_parameter=is_parameter,
                )
            else:
                bounding_box = AxisAlignedBoundingBox.create_at_random_position_scaled(
                    min_distance_from_origin=10,
                    max_distance_from_origin=20,
                    object_scale=torch.tensor(  # pylint: disable=not-callable
                        [scale[0], scale[1], scale[2]]
                    ),
                )
            if is_parameter:
                dict_items.append((object_perception, bounding_box))
            else:
                excluded_main_objects.append((object_perception, bounding_box))

        return PositioningModel(
            immutabledict(dict_items),
            immutabledict(excluded_main_objects),
            sub_object_mapping,
            in_region_relations,
        )

    @staticmethod
    def _create_sub_objs_to_bounding_boxes(
        sub_objects: Mapping[str, Mapping[str, LPoint3f]],
    ) -> Mapping[str, Mapping[str, AxisAlignedBoundingBox]]:
        # create bounding boxes for sub-objects

        parent_to_child: DefaultDict[
            str, Mapping[str, AxisAlignedBoundingBox]
        ] = defaultdict(dict)

        for parent_obj, sub_obj_map in sub_objects.items():
            sub_object_to_aabb = {}
            for sub_obj, offset in sub_obj_map.items():
                sub_object_to_aabb[
                    sub_obj
                ] = AxisAlignedBoundingBox.create_at_center_point_scaled(
                    center=np.array([offset.x, offset.y, offset.z], dtype=float),
                    # TODO: FIX THIS WITH REAL SCALE
                    object_scale=torch.ones(3),  # pylint: disable=not-callable
                    is_parameter=False,
                    offset=np.array([offset.x, offset.y, offset.z], dtype=float),
                )

            parent_to_child[parent_obj] = sub_object_to_aabb
        return parent_to_child

    def forward(self):  # pylint: disable=arguments-differ
        self._update_subobject_positions()

        collision_penalty = sum(
            self.collision_penalty(box1, box2)
            for (box1, box2) in combinations(self.included_and_excluded_boxes, 2)
        )
        below_ground_penalty = sum(
            self.below_ground_penalty(box) for box in self.object_bounding_boxes
        )
        weak_gravity_penalty = sum(
            self.weak_gravity_penalty(
                bounding_box, immutableset(self.in_region_relations[object_perception])
            )
            for object_perception, bounding_box in self.object_perception_to_bounding_box.items()
            if object_perception in self.in_region_relations
        )
        in_region_penalty = sum(
            self.in_region_penalty(
                object_perception,
                immutableset(self.in_region_relations[object_perception]),
            )
            for object_perception in self.object_perception_to_bounding_box
            if object_perception in self.in_region_relations
        )
        # print(
        #     f"collision penalty: {collision_penalty}"
        #     f"\nout of bounds penalty: {below_ground_penalty}"
        #     f"\ngravity penalty: {weak_gravity_penalty}"
        #     f"\nin-region penalty: {in_region_penalty}"
        # )
        return (
            collision_penalty
            + below_ground_penalty
            + weak_gravity_penalty
            + in_region_penalty
        )

    def dump_object_positions(self, *, prefix: str = "") -> None:
        for (
            object_perception,
            bounding_box,
        ) in self.object_perception_to_bounding_box.items():
            print(
                f"{prefix}{object_perception.debug_handle} = {bounding_box.center.data}\n{prefix}scale:{bounding_box.scale.data}"
            )
        # print("Sub-object bounding box positions:")
        # for (
        #     main_object,
        #     sub_object_to_bounding_box,
        # ) in self.object_to_sub_object_to_bounding_box.items():
        #     print(main_object)
        #     for (sub_object, bounding_box) in sub_object_to_bounding_box.items():
        #         print(
        #             f"\t{sub_object} = {bounding_box.center.data}\n\t{prefix}scale:{bounding_box.scale.data}"
        #         )

    def get_object_position(self, obj: ObjectPerception) -> torch.Tensor:
        """
        Retrieves the (center) position of an AdamObject contained in this model.
        Args:
            obj: AdamObject whose position is requested

        Returns: (3,) tensor of the requested object's position.

        Raises KeyError if an AdamObject not contained in this model is queried.
        """
        return self.object_perception_to_bounding_box[obj].center.data

    def get_objects_positions(self) -> PositionsMap:
        """
        Retrieves positions of all AdamObjects contained in this model.
        Returns: PositionsList

        """
        position_pairs: List[Tuple[str, torch.Tensor]] = []
        scale_pairs: List[Tuple[str, torch.Tensor]] = []
        for (
            object_perception,
            bounding_box,
        ) in self.object_perception_to_bounding_box.items():
            position_pairs.append(
                (object_perception.debug_handle, bounding_box.center.data)
            )
            scale_pairs.append((object_perception.debug_handle, bounding_box.scale.data))
        for (
            object_perception,
            bounding_box,
        ) in self.object_perception_to_excluded_bounding_box.items():
            position_pairs.append(
                (object_perception.debug_handle, bounding_box.center.data)
            )
            scale_pairs.append((object_perception.debug_handle, bounding_box.scale.data))

        return PositionsMap(immutabledict(position_pairs), immutabledict(scale_pairs))

    def _update_subobject_positions(self) -> None:
        for main_object, main_aabb in self.object_perception_to_bounding_box.items():
            for _, sub_aabb in self.object_to_sub_object_to_bounding_box[
                main_object.debug_handle
            ].items():
                sub_aabb.center = sub_aabb.offset + main_aabb.center

class BelowGroundPenalty(nn.Module):  # type: ignore
    """
    Model that penalizes boxes lying outside of the scene (i.e. below the ground plane) or off-camera)
    """

    def __init(self) -> None:  # pylint: disable=useless-super-delegation
        super().__init__()

    def forward(  # type: ignore
        self, bounding_box: AxisAlignedBoundingBox
    ):  # pylint: disable=arguments-differ
        distance_above_ground = bounding_box.z_coordinate_of_lowest_corner()
        if distance_above_ground >= 0:
            return 0
        else:
            return -distance_above_ground

class WeakGravityPenalty(nn.Module):  # type: ignore
    """
    Model that penalizes boxes that are not resting on the ground.
    """

    # TODO: exempt birds from this constraint https://github.com/isi-vista/adam/issues/485

    def __init__(
        self,
        object_perception_to_bounding_box: Mapping[
            ObjectPerception, AxisAlignedBoundingBox
        ],
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
    ) -> None:  # pylint: disable=useless-super-delegation
        super().__init__()
        self.object_perception_to_bounding_box = object_perception_to_bounding_box
        self.in_region_relations = in_region_relations
        self.ground_region = Region(
            GROUND_PERCEPTION, EXTERIOR_BUT_IN_CONTACT, GRAVITATIONAL_UP
        )

    def forward(  # type: ignore
        self,
        bounding_box: AxisAlignedBoundingBox,
        designated_regions: ImmutableSet[Region[ObjectPerception]],
    ):  # pylint: disable=arguments-differ

        ground_region = None
        for region in designated_regions:
            if region.reference_object.debug_handle == "the ground":
                ground_region = region
        # if this object is not supposed to be on the ground, don't apply the gravity constraint.
        if not ground_region:
            return 0.0

        distance_above_ground = bounding_box.z_coordinate_of_lowest_corner()
        if ground_region.distance == EXTERIOR_BUT_IN_CONTACT:
            if distance_above_ground <= 0:
                return 0.0
            else:
                # a linear penalty leads to a constant gradient, just like real gravity
                return GRAVITY_PENALTY * distance_above_ground
        elif ground_region.distance == DISTAL:
            if distance_above_ground < DISTAL_MIN_DISTANCE:
                return GRAVITY_PENALTY * DISTAL_MIN_DISTANCE - distance_above_ground
        elif ground_region.distance == PROXIMAL:
            if PROXIMAL_MIN_DISTANCE <= distance_above_ground <= PROXIMAL_MAX_DISTANCE:
                return 0.0
            elif distance_above_ground < PROXIMAL_MIN_DISTANCE:
                return GRAVITY_PENALTY * PROXIMAL_MIN_DISTANCE - distance_above_ground
            else:
                return GRAVITY_PENALTY * distance_above_ground - PROXIMAL_MAX_DISTANCE
        return 0.0

class CollisionPenalty(nn.Module):  # type: ignore
    """
    Model that penalizes boxes that are colliding with other boxes.
    """

    def __init__(self):  # pylint: disable=useless-super-delegation
        super().__init__()

    def forward(  # type: ignore
        self,
        bounding_box_1: AxisAlignedBoundingBox,
        bounding_box_2: AxisAlignedBoundingBox,
    ):  # pylint: disable=arguments-differ

        # get face norms from one of the boxes:
        face_norms = bounding_box_2.face_normal_vectors()

        return CollisionPenalty.overlap_penalty(
            CollisionPenalty.get_min_max_overlaps(
                CollisionPenalty.get_min_max_corner_projections(
                    bounding_box_1.corners_onto_axes_projections(face_norms)
                ),
                CollisionPenalty.get_min_max_corner_projections(
                    bounding_box_2.corners_onto_axes_projections(face_norms)
                ),
            )
        )

    @staticmethod
    def get_min_max_corner_projections(projections: torch.Tensor):
        """
        Retrieve the minimum and maximum corner projection (min/max extent in that dimension) for each axis
        Args:
            projections: Tensor(3, 8) -> corner projections onto each of three dimensions

        Returns:
            Tensor(3, 2) -> (min, max) values for each of three dimensions

        """
        check_arg(projections.shape == (3, 8))

        min_indices = torch.min(projections, 1)
        max_indices = torch.max(projections, 1)
        # these are tuples of (values, indices), both of which are tensors

        # helper variable for representing dimension numbers
        # see https://github.com/pytorch/pytorch/issues/24807 re: pylint issue
        dims = torch.tensor([0, 1, 2], dtype=torch.int)  # pylint: disable=not-callable
        # select the indexed items (from a 24 element tensor)
        minima = torch.take(projections, min_indices[1] + (dims * 8))
        maxima = torch.take(projections, max_indices[1] + (dims * 8))
        # stack the minim
        return torch.stack((minima, maxima), 1)

    @staticmethod
    def get_min_max_overlaps(
        min_max_proj_0: torch.Tensor, min_max_proj_1: torch.Tensor
    ) -> torch.Tensor:
        """
        Given min/max corner projections onto 3 axes from two different objects,
        return an interval for each dimension representing the degree of overlap or
        separation between the two objects.
        Args:
            min_max_proj_0: Tensor(3,2) min_max_projections for box 0
            min_max_proj_1: Tensor(3,2) min_max projections for box 1

        Returns:
            (3, 2) tensor -> ranges (start, end) of overlap OR separation in each of three dimensions.
            If (start - end) is positive, this indicates that the boxes do not overlap along this dimension,
            otherwise, a negative value indicates an overlap along that dimension.
        """
        check_arg(min_max_proj_0.shape == (3, 2))
        check_arg(min_max_proj_1.shape == (3, 2))

        # see https://github.com/pytorch/pytorch/issues/24807 re: pylint issue
        dims = torch.tensor([0, 1, 2], dtype=torch.int)  # pylint: disable=not-callable

        mins_0 = min_max_proj_0.gather(1, torch.zeros((3, 1), dtype=torch.long))
        mins_1 = min_max_proj_1.gather(1, torch.zeros((3, 1), dtype=torch.long))

        combined_mins = torch.stack((mins_0, mins_1), 1).squeeze()
        max_indices = torch.max(combined_mins, 1)
        maximum_mins = torch.take(combined_mins, max_indices[1] + (dims * 2))

        # should stick together the minimum parts and the maximum parts
        # with columns like:
        # [ min0x   min1x
        #   min0y   min1y
        #   min0z   min1z
        #                ]
        # then find the maximum element from each row

        # repeat the process for the min of the max projections
        maxs_0 = min_max_proj_0.gather(1, torch.ones((3, 1), dtype=torch.long))
        maxs_1 = min_max_proj_1.gather(1, torch.ones((3, 1), dtype=torch.long))
        combined_maxes = torch.stack((maxs_0, maxs_1), 1).squeeze()
        min_indices = torch.min(combined_maxes, 1)
        minimum_maxes = torch.take(combined_maxes, min_indices[1] + (dims * 2))

        return torch.stack((maximum_mins, minimum_maxes), 1)

    @staticmethod
    def overlap_penalty(min_max_overlaps: torch.Tensor) -> torch.Tensor:
        """
        Return penalty depending on degree of overlap between two 3d boxes.
        Args:
            min_max_overlaps: (3, 2) tensor -> intervals describing degree of overlap between the two boxes

        Returns: Tensor with a positive scalar of the collision penalty, or tensor with zero scalar
        for no collision.
        """
        check_arg(min_max_overlaps.shape == (3, 2))
        # subtract each minimum max from each maximum min:
        overlap_distance = min_max_overlaps[:, 0] - min_max_overlaps[:, 1]

        # as long as at least one dimension's overlap distance is positive (not overlapping),
        # then the boxes are not colliding
        for dim in range(3):
            if overlap_distance[dim] >= 0:
                return torch.zeros(1, dtype=torch.float)

        # otherwise the penetration distance is the maximum negative value
        # (the smallest translation that would disentangle the two

        # overlap is represented by a negative value, which we return as a positive penalty
        return overlap_distance.max() * -1 * COLLISION_PENALTY

class InRegionPenalty(nn.Module):  # type: ignore
    """ Model that penalizes boxes for not adhering to relational (distance and direction)
        constraints with other boxes -- for being outside of the Region it is supposed to occupy
    """

    def __init__(
        self,
        object_perception_to_bounding_box: Mapping[
            ObjectPerception, AxisAlignedBoundingBox
        ],
        object_perception_to_excluded_bounding_box: Mapping[
            ObjectPerception, AxisAlignedBoundingBox
        ],
        sub_object_to_bounding_box: Mapping[str, AxisAlignedBoundingBox],
        in_region_relations: Mapping[ObjectPerception, List[Region[ObjectPerception]]],
    ) -> None:  # pylint: disable=useless-super-delegation
        super().__init__()
        self.handle_to_bounding_box: Mapping[str, AxisAlignedBoundingBox] = {}
        for object_perception, aabb in object_perception_to_bounding_box.items():
            self.handle_to_bounding_box[object_perception.debug_handle] = aabb
        for (
            object_perception,
            excluded_aabb,
        ) in object_perception_to_excluded_bounding_box.items():
            self.handle_to_bounding_box[object_perception.debug_handle] = excluded_aabb
        for sub_object, aabb in sub_object_to_bounding_box.items():
            self.handle_to_bounding_box[sub_object] = aabb
        self.in_region_relations = in_region_relations

    def forward(  # type: ignore
        self,
        target_object: ObjectPerception,
        designated_region: ImmutableSet[Region[ObjectPerception]],
    ):  # pylint: disable=arguments-differ

        # print(f"{target_object.debug_handle} positioned w/r/t {designated_region}")

        # return 0 if object has no relative positions to apply
        if not designated_region:
            # print(f"{target_object.debug_handle} has no relative positioning constraints")
            return torch.zeros(1)

        return sum(
            self.penalty(
                self.handle_to_bounding_box[target_object.debug_handle],
                self.handle_to_bounding_box[region.reference_object.debug_handle],
                region,
            )
            # positioning w/r/t the ground is handled by other constraints
            for region in designated_region
            if region.reference_object.debug_handle != "the ground"
        )

    def penalty(
        self,
        target_box: AxisAlignedBoundingBox,
        reference_box: AxisAlignedBoundingBox,
        region: Region[ObjectPerception],
    ):
        """
        Assign a penalty for target_box if it does not comply with its relation to reference_box according to
        the degree of difference between the expected angle between the boxes and the expected distance between
        the two objects.
        Args:
            target_box: box to be penalized if positioned outside of region
            reference_box: box referred to by region
            region: region that target_box should be in

        Returns: Tensor(1,) with penalty

        """
        # print(
        #     f"TARGET: {target_box.center} REFERENCE: {reference_box.center} REGION:{region}"
        # )
        assert region.distance is not None
        # get direction that box 1 should be in w/r/t box 2
        # TODO: allow for addressee directions

        # if direction is not provided, this vector is zero
        direction_vector = self.direction_as_unit_vector(region.direction, reference_box)

        current_direction_from_reference_to_target = (
            target_box.center - reference_box.nearest_center_face_point(target_box.center)
        )

        angle = angle_between(
            direction_vector, current_direction_from_reference_to_target
        )
        if not angle or torch.isnan(angle):
            angle = torch.zeros(1, dtype=torch.float)

        distance = target_box.nearest_center_face_distance_from_point(
            reference_box.center
        )

        # distal has a minimum distance away from object to qualify
        if region.distance == DISTAL:
            if distance < DISTAL_MIN_DISTANCE:
                distance_penalty = DISTAL_MIN_DISTANCE - distance
            else:
                distance_penalty = torch.zeros(1)
        # proximal has a min/max range
        elif region.distance == PROXIMAL:
            if PROXIMAL_MIN_DISTANCE <= distance <= PROXIMAL_MAX_DISTANCE:
                distance_penalty = torch.zeros(1)
            elif distance < PROXIMAL_MIN_DISTANCE:
                distance_penalty = PROXIMAL_MIN_DISTANCE - distance
            else:
                distance_penalty = distance - PROXIMAL_MAX_DISTANCE

        # exterior but in contact has a tiny epsilon of acceptable distance
        # assuming that collisions are handled elsewhere
        elif region.distance == EXTERIOR_BUT_IN_CONTACT:
            if distance > EXTERIOR_BUT_IN_CONTACT_EPS:
                distance_penalty = distance * 1.5
            else:
                distance_penalty = torch.zeros(1)
        else:
            raise RuntimeError(
                "Currently unable to support Interior distances w/ positioning solver"
            )

        if angle and angle.allclose(PI):
            return distance_penalty * DISTANCE_PENALTY

        # print(f"ANGLE: {angle}, DISTANCE: {distance}")

        return angle * ANGLE_PENALTY + distance_penalty * DISTANCE_PENALTY

    def direction_as_unit_vector(
        self,
        direction: Optional[Direction[ObjectPerception]],
        direction_reference: AxisAlignedBoundingBox,
        addressee_reference: Optional[AxisAlignedBoundingBox] = None,
    ) -> torch.Tensor:
        """
        Convert a direction to a unit vector (3,) tensor to represent the direction.
        Args:
            direction: Direction object
            direction_reference: AABB corresponding to the object referenced by the direction parameter
            addressee_reference: AABB corresponding the an addressee referenced by the direction parameter

        Returns: (3,) Tensor. A unit vector describing a direction.

        """
        if direction is None:
            return torch.zeros(3)
        # special case: gravity
        if direction == GRAVITATIONAL_UP:
            return torch.tensor(  # pylint: disable=not-callable
                [0, 0, 1], dtype=torch.float
            )
        elif direction == GRAVITATIONAL_DOWN:
            return torch.tensor(  # pylint: disable=not-callable
                [0, 0, -1], dtype=torch.float
            )

        # horizontal axes mapped to world axes (as one of many possible visualizations)
        # We make the executive decision to map a horizontal relationship onto the X axis,
        # the better to view from a static camera position.
        # TODO: change to reflect distance from box extents https://github.com/isi-vista/adam/issues/496
        if isinstance(direction.relative_to_axis, HorizontalAxisOfObject):
            if direction.positive:
                return torch.tensor(  # pylint: disable=not-callable
                    [1, 0, 0], dtype=torch.float
                )

            else:
                return torch.tensor(  # pylint: disable=not-callable
                    [-1, 0, 0], dtype=torch.float
                )

        # in this case, calculate a vector facing toward or away from the addressee
        if (
            isinstance(direction.relative_to_axis, FacingAddresseeAxis)
            and addressee_reference is not None
        ):
            if direction.positive:
                # pointing toward addressee
                return addressee_reference.center - direction_reference.center
            else:
                # pointing away from addressee
                return direction_reference.center - addressee_reference.center

        raise NotImplementedError(f"direction_to_world called with {direction}")

def angle_between(vector0: torch.Tensor, vector1: torch.Tensor) -> Optional[torch.Tensor]:
    """
    Returns angle between two vectors (tensors (3,) )
    Args:
        vector0: tensor (3,)
        vector1: tensor (3,)

    Returns: tensor (1,) with the angle (in radians) between the two vectors.
           Will return NaN if either of the inputs is zero.
    """
    if torch.nonzero(vector0).size(0) == 0 or torch.nonzero(vector1).size(0) == 0:
        return None
    unit_vector0 = vector0 / torch.norm(vector0, 2)
    unit_vector1 = vector1 / torch.norm(vector1, 2)
    return unit_vector0.dot(unit_vector1).acos()

if __name__ == "__main__":
    main()


from pylab import *
from matplotlib.patches import FancyArrow

# Foreman-Mackey's taste in figures
rc("font", size=20, family="serif", serif="Computer Sans")
rc("text", usetex=True)

import word
import parameters

t = linspace(-10., 10., 10001)
plot(t, word.TwoExp(t).model(parameters.TwoExpParameters(1.0, 0.5, 1., 3., log=False)), linewidth=2)
xlabel('Time (seconds)')
ylabel('Poisson Rate')
ylim([0., 1.1])
axvline(1., color='r', linestyle='--')
title('A Word')

# Build an arrow.
ar1 = FancyArrow(1., 1.01*exp(-1.), -0.5, 0., length_includes_head=True,
                color='k', head_width=0.01, head_length=0.2, width=0.001, linewidth=1)
ar2 = FancyArrow(1., 0.99*exp(-1.), 1.5, 0., length_includes_head=True,
                color='k', head_width=0.01, head_length=0.2, width=0.001, linewidth=1)
ax = gca()
# Add the arrow to the axes.
ax.add_artist(ar1)
ax.add_artist(ar2)

# Add text
text(-0.4, 1.*exp(-1.), r'$\tau$')
text(2.7, 1.*exp(-1.), r'$\tau S$')

savefig('documents/word.pdf', bbox_inches='tight')
show()


from Calc import calc

if __name__ == '__main__':

    number_operation = input("Введіть кількість операцій: \t")

    try:
        number_operation = int(number_operation)
    except ValueError:
        print("Ви ввели не вірне значеня!")
    else:

        if number_operation > 0:
            i = 2

            first_number = input("Введіть перше число: \t")
            operation = input("Оберіть операцію: Додавння(+), Віднімання(-), Множення(*), Ділення(/) \t")
            second_number = input("Введіть друге число: \t")

            result = calc(first_number=first_number,
                          second_number=second_number,
                          operation=operation)

            if isinstance(result, str):
                print(result)
            else:
                while i < number_operation:
                    first_number = result
                    operation = input("Оберіть операцію: Додавння(+), Віднімання(-), Множення(*), Ділення(/) \t")
                    second_number = input("Введіть наступне число: \t")
                    if isinstance(result, str):
                        break
                    if operation not in ("+", "-", "*", "/"):
                        break

                    result = calc(first_number=first_number,
                                  second_number=second_number,
                                  operation=operation)

                    i += 1

                print(result)
        else:
            print("Щось не те ввів, поміркуй")


# Aaron Yerke, HW 2 for ML 2019
# 1. (50 points) Implement gradient descent-based logistic regression in Python. Use
# ∆J = 0.00001 as the stopping criterion.
# 2. (50 points total distributed as below) Apply your code from question 2 to the iris virginica and virsicolor flowers.
# Specifically, randomly select 99 of these flowers for training your logistic model and use the remaining one flower for testing.
# You only need to do training once and testing once with your specific choice of the training flowers and testing flowers.
# That is to say, you don’t need to do the leave-one-out cross validation 100 times.
# (a) (15 points) After your training, plot the total cost J vs iterations for your 99 training flowers for four scenarios.
# (b) (20 points) Predict the flower type of your testing flower for each of the four scenarios.
# (c) (15 points) Apply sklearn.linear model.LogisticRegression to your specific choice of training flowers. With the intercept and coefficients produced by sklearn, calculate the total final cost J for your 99 flowers.

# --------------------------------------------------------------------------
# Import external libraries
# --------------------------------------------------------------------------
# for my code
import os, sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from itertools import chain
# for comparison
import scipy.stats as stats
from sklearn import linear_model
from sklearn.metrics import f1_score
# for dataset
from sklearn.datasets import load_iris
from sklearn import preprocessing

# --------------------------------------------------------------------------
# set up plotting parameters
# --------------------------------------------------------------------------
line_width_1 = 2
line_width_2 = 2
marker_1 = '.' # point
marker_2 = 'o' # circle
marker_size = 12
line_style_1 = ':' # dotted line
line_style_2 = '-' # solid line

# --------------------------------------------------------------------------
# Some useful functions
# --------------------------------------------------------------------------
def logit(p):
    return np.log(p/(1-p))

def sigmoid(line):
    return 1/(1 + np.exp(-line))

def loss_j1(X,y, coef):
    return 1/len(y) * np.sum((y @ np.log(sigmoid(X@coef)) - (1-y) @ np.log(1-sigmoid(X@coef)) ))

def loss_j(y, y_hat):
    return -np.mean(y*np.log(y_hat) + (1-y) * np.log(1-y_hat))

# --------------------------------------------------------------------------
# set up log reg class
# --------------------------------------------------------------------------
class my_logistic_reg:
    def __init__(self, lr = 0.001, n_iter = 1000, dj_stop = 0.00001):
        self.slopes = None
        self.y_intercept = None
        self.lr = lr
        self.n_iter = n_iter
        self.dj_stop = dj_stop

    def _loss_j(self, y, y_hat):
        return -np.mean(y*np.log(y_hat) + (1-y) * np.log(1-y_hat))

    def fit_model(self, my_x, my_y):
        n_samples, n_features = my_x.shape
        # init parameters
        self.slopes = np.zeros(n_features)
        self.y_intercept = 1
        self.cost_j = []

        # gradient descent
        while len(self.cost_j) < self.n_iter:#this is basically a for-loop that will go n iterations
            # approximate y with linear combination of slope and x, plus y_intercept
            lin_model = np.dot(my_x, self.slopes) + self.y_intercept
            # apply sigmoid function
            y_predicted = sigmoid(lin_model)
            loss = self._loss_j(my_y, y_predicted)
            # compute gradients
            dz = y_predicted -my_y
            d_slope = (1 / n_samples) * np.matmul(my_x.T, dz)
            d_intercept = np.sum(dz)

            # update parameters
            self.slopes -= self.lr * d_slope
            self.y_intercept -= self.lr * d_intercept
            if len(self.cost_j) == 0:
                self.cost_j.append(loss)
            else:
                self.cost_j.append(loss)
                if abs(loss - self.cost_j[-2]) < self.dj_stop:
                    break#get out of while loop
        print("Fit completed!")

    def test_model(self, my_x, y_val = None):
        lin_model = np.dot(my_x, self.slopes) + self.y_intercept
        y_predicted = sigmoid(lin_model)
        if y_predicted > 0.5:
            model_prediction = 1
        else:
            model_prediction = 0

        if y_val != None:
            print(f"The model prediction: {model_prediction}\nThe correct value: {y_val}")
        return model_prediction

    def plot_cost(self):
        if len(self.cost_j) != 0:
            fig = plt.figure()
            ax = fig.add_subplot(1, 1, 1)
            ax.scatter( range(0, len(self.cost_j) ), self.cost_j, label=f'Cost function values', color='red', marker=marker_1, linewidth=line_width_1)
#            ax.plot(self.x, self.y_hat, color='blue', label='model from training data', linewidth=line_width_1)
            ax.set_xlabel('Iterations')
            ax.set_ylabel('Cost values')
#            ax.set_title("Linear regression of diabetes data with d_lm class")
#            ax.legend(loc='lower right', fontsize=9)
            fig.show()

# --------------------------------------------------------------------------
# create training and testing datasets
# --------------------------------------------------------------------------
iris = load_iris()
df = pd.DataFrame(iris.data, columns = iris['feature_names'])
df['species'] = pd.Categorical.from_codes(iris.target, iris.target_names)
df = df[df.species != "setosa"]
df = df.reset_index(drop = True)
df['species'] = df['species'].map({'versicolor': 1, 'virginica': 0})
df.to_csv('testing.tsv', sep='\t')
#print(df.shape[0])

df_x = df.iloc[:,list(range(df.shape[1]-1))]
#normalize data to max value
x = df_x.values #returns a numpy array
min_max_scaler = preprocessing.MinMaxScaler()
x_scaled = min_max_scaler.fit_transform(x)
df_x = pd.DataFrame(x_scaled)
print(f'df_x.shape: {df_x.shape}')
df_y = df.iloc[:,df.shape[1]-1]
print(f'df_y.shape: {df_y.shape}')

test_index = np.random.randint(low = 0, high = df.shape[0] - 1, size = 1 )[0]
print(test_index)
train_index = filter(lambda a: a != test_index, range(df.shape[0]))
print(train_index)

train_x = df_x.drop(test_index, axis = 0)
print(f'train_x.shape: {train_x.shape}')
train_y = df_y.drop(test_index, axis = 0)
print(f'train_y.shape: {train_y.shape}')
test_x = df_x.drop(train_index, axis = 0)
print(f'test_x.shape: {test_x.shape}')
test_y = df_y.drop(filter(lambda a: a != test_index, range(df.shape[0])), axis = 0)
print(f'test_y.shape: {test_y.shape}')

# --------------------------------------------------------------------------
# Fit and test homemade model
# --------------------------------------------------------------------------

my_lr = my_logistic_reg()
my_lr.fit_model(train_x, train_y)
my_lr.plot_cost()
my_lr.test_model(test_x, test_y.iloc[0])

# --------------------------------------------------------------------------
# Test homemade model with f1_score
# --------------------------------------------------------------------------
preds = []
for i in train_x.T.iteritems():
    preds.append(my_lr.test_model(np.matrix(i[1])))
print("accuracy by homemade model")
print(f1_score(preds,train_y))
f1_score(preds,train_y)

# --------------------------------------------------------------------------
# run professional log reg model
# --------------------------------------------------------------------------
print('pro_stuff')
skl_log = linear_model.LogisticRegression(solver="lbfgs")
skl_log.fit(X=train_x, y=train_y)
skl_pred = skl_log.predict(test_x)
print(f'sklearn log prediction: {skl_pred}')
print(f'Correct answer: {test_y}, {test_y == skl_pred}')
print(skl_log.coef_)
print(skl_log.intercept_)

preds = []
for i in train_x.T.iteritems():
    preds.append(skl_log.predict(np.matrix(i[1])))
print("accuracy by sklearn model")
print(f1_score(preds,train_y))
f1_score(preds,train_y)

skl_log_cost = loss_j1(train_x, train_y, skl_log.coef_.T)
print(f'2(c) the total final cost J: {skl_log_cost}')


import bluetooth
import time
import serial  # 导入模块
import zxing
import numpy as np
import time
import cv2 as cv

reader = zxing.BarCodeReader()
log_counter = 0
start_time = time.time()
IMG_PATH='/home/pi/Documents/gx/0003.jpg'
# set blue thresh 设置HSV中蓝色、天蓝色范围
lower_red = np.array([0,43,46])
upper_red = np.array([15,255,255])
# lower_blue
lower_blue=np.array([95, 43, 46])
upper_blue=np.array([ 124, 255, 255])

lower_g=np.array([35, 43, 46])
upper_g=np.array([ 77, 255, 255])

UPPER_BOUND=600#130#600
LOWER_BOUND=1700#400#1700
def voice(txt, portx="/dev/ttyUSB0", bps=9600, timex=5,):
    try:
        # #端口，GNU / Linux上的/ dev / ttyUSB0 等 或 Windows上的 COM3 等
        # portx="/dev/ttyUSB0"
        # #波特率，标准值之一：50,75,110,134,150,200,300,600,1200,1800,2400,4800,9600,19200,38400,57600,115200
        # bps=9600
        # #超时设置,None：永远等待操作，0为立即返回请求结果，其他值为等待超时时间(单位为秒）
        # timex=5
        # # 打开串口，并得到串口对象
        ser = serial.Serial(portx, bps, timeout=timex)
        # 写数据
        result = ser.write(("<G>"+txt).encode("gbk"))
        print("写总字节数:", result)
        ser.close()  # 关闭串口
    except Exception as e:
        print("---异常---：", e)
    return

def scanQRCode(ImgPAth):
    barcode = reader.decode(ImgPAth)
    # logPrinter()
    print(barcode)
    print(barcode.raw)
    print(barcode.parsed)
    return barcode.parsed

reader = zxing.BarCodeReader()
log_counter = 0
time_start = time.time()

def logPrinter(logtxt):
    global log_counter
    # global start_time
    end_time = time.time()
    print("log: %d time %.3f  --  %s" %
          (end_time-start_time, log_counter, logtxt))
    log_counter += 1
    return

import numpy as np
import cv2
import time
def catch_picture():
    #调用笔记本内置摄像头，所以参数为0，如果有其他的摄像头可以调整参数为1，2
    start_time=time.time()
    cap=cv2.VideoCapture(2)
    while True:
        #从摄像头读取图片
        sucess,img=cap.read()
        #转为灰度图片
        # gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
        #显示摄像头，背景是灰度。
        # cv2.imshow("img",gray)
        #保持画面的持续。
        # k=cv2.waitKey(1)
        end_time=time.time()
        if end_time-start_time>2:
            cv2.imwrite(IMG_PATH,img)
            # cv2.destroyAllWindows()
            break
    return
# def if_qrcode():

#     return
def classify_color():
    frame=cv.imread(IMG_PATH)

    frame=frame[UPPER_BOUND:LOWER_BOUND][:][:]
    frame=cv2.resize(frame,(480,100))
    # cv.imshow('Capture', frame)
    # change to hsv model
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)

    # get mask 利用inRange()函数和HSV模型中蓝色范围的上下界获取mask，mask中原视频中的蓝色部分会被弄成白色，其他部分黑色。

    mask_b = cv.inRange(hsv, lower_blue, upper_blue)
    print(mask_b.shape)
    mask_r = cv.inRange(hsv, lower_red, upper_red)
    mask_g = cv.inRange(hsv, lower_g,upper_g)
    res_r = cv.bitwise_and(frame, frame, mask=mask_r)
    res_g = cv.bitwise_and(frame, frame, mask=mask_g)
    res_b = cv.bitwise_and(frame, frame, mask=mask_b)
    cv.imwrite('/home/pi/Documents/gx/r.jpg',res_r)
    cv.imwrite('/home/pi/Documents/gx/g.jpg',res_g)
    cv.imwrite('/home/pi/Documents/gx/b.jpg',res_b)
    kernel=np.ones((5,5),np.uint8)
    mask_b=cv.erode(mask_b,kernel,iterations=5)
    mask_r=cv.erode(mask_r,kernel,iterations=5)
    mask_g=cv.erode(mask_g,kernel,iterations=5)
    # mask_b=1-mask_b
    # mask_r=1-mask_r
    # mask_g=1-mask_g
    X_axis=np.arange(0,frame.shape[1],1)
    # mask=cv.erode(mask,kernel,iterations=1)
    mask_r=mask_r>0
    mask_g=mask_g>0
    mask_b=mask_b>0
    r_sum=(np.sum(np.nonzero(mask_r))-1)
    g_sum=(np.sum(np.nonzero(mask_g))-1)
    b_sum=(np.sum(np.nonzero(mask_b))-1)
    x_r=(np.multiply(X_axis,mask_r[:]).sum()/r_sum)if r_sum>200 else -1
    x_g=(np.multiply(X_axis,mask_g[:]).sum()/g_sum)if g_sum>200 else -1
    x_b=(np.multiply(X_axis,mask_b[:]).sum()/b_sum)if b_sum>200 else -1
    # res = cv.bitwise_and(frame, frame, mask=mask1)
    # cv.imwrite('det000.jpg',res)
    # # if __name__ == '__main__':
    # while True:
    #     cv.imshow('img', mask)
    #     if cv.waitKey() == ord('q'):
    #         break
    # cv.destroyAllWindows()

    return (x_r,x_g,x_b)
# def catch_

HOST_MAC_ADDR = 'DC:A6:32:AA:75:A3'  # 目的蓝牙的地址
sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
port = 1

if __name__ == "__main__":
    # try:
    print(classify_color())
    # return
#     sock.connect((HOST_MAC_ADDR, port))  # 连接蓝牙

#     while True:
#         now_time=time.time()
#         now_time-=start_time
#         print('now time')
#         # 使用计时或者串口回传
#         if now_time<10:
#             logPrinter('直线')
#         elif now_time<15:
#             logPrinter('拍照')
#             sock.send(('<G>'+scanQRCode(IMG_PATH)).encode())
#         elif now_time<30:
#             logPrinter('直线')
#         elif now_time<45:
#             logPrinter('左横')
#         else:
#             break
#         # sock.send('hello!'.encode()) #每隔三秒发送一个字符串
#             # time.sleep(3)
#     # except:
#     #     sock.close()
#     #     logPrinter('disconnect')
#     sock.close()
#     logPrinter('disconnect')
#     logPrinter('task finished')
# # if __name__ == "__main__":
# #     reader = zxing.BarCodeReader()
# #     log_counter=0
# #     time_start=time.time()
# #     # voice("绿色蓝色")
# #     logPrinter("000")


#!/usr/bin/env python

import os
import json
import argparse
from pprint import pprint
from pathlib import Path

import requests
from requests.auth import HTTPBasicAuth
import jwt
from dotenv import find_dotenv, load_dotenv

DOTENV_PATH = find_dotenv()
if DOTENV_PATH:
    load_dotenv(DOTENV_PATH)

KEYCLOAK_HOST = os.environ.get("KEYCLOAK_HOST") or "localhost"
KEYCLOAK_PORT = os.environ.get("KEYCLOAK_PORT") or "8080"
KEYCLOAK_ISSUER = (
    os.environ.get("KEYCLOAK_ISSUER") or
    f"http://{KEYCLOAK_HOST}:{KEYCLOAK_PORT}/realms/fhir-dev"
)
KEYCLOAK_CLIENT_ID = (
    os.environ.get("KEYCLOAK_CLIENT_ID") or "fhir-superuser-client"
)
KEYCLOAK_CLIENT_SECRET = os.environ.get("KEYCLOAK_CLIENT_SECRET") or "none"
SMILECDR_HOST = os.environ.get("SMILECDR_HOST") or "localhost"
SMILECDR_PORT = os.environ.get("SMILECDR_PORT") or "8000"
SMILECDR_FHIR_ENDPOINT = "http://{SMILECDR_HOST}:{SMILECDR_PORT}"
SMILECDR_AUDIENCE = "https://kf-api-fhir-smilecdr-dev.org"

def send_request(method, *args, **kwargs):
    print("\n***** Sending request ******")
    try:
        requests_op = getattr(requests, method)
        resp = requests_op(*args, **kwargs)
        resp.raise_for_status()
    except requests.exceptions.HTTPError as e:
        print("Problem sending request to endpoint")
        print(resp.text)
        raise e

    return resp

def get_access_token(
    client_id=KEYCLOAK_CLIENT_ID, client_secret=KEYCLOAK_CLIENT_SECRET,
    issuer=KEYCLOAK_ISSUER, decoded=True
):
    """
    Test OAuth2 stuff
    """
    headers = {
        "Content-Type": "application/json",
    }
    # Get OIDC configuration
    print("\n****** Get OIDC Configuration *************")
    openid_config_endpoint = (
        f"{issuer}/.well-known/openid-configuration"
    )
    resp = send_request("get", openid_config_endpoint, headers=headers)
    openid_config = resp.json()

    pprint(openid_config)

    # Authorize to get access token
    print("\n****** Get Access Token *************")
    token_endpoint = openid_config["token_endpoint"]
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "audience": SMILECDR_AUDIENCE
    }
    params = {
        "scope": "fhir"
    }
    resp = send_request("post", token_endpoint, data=payload, params=params)
    token_payload = resp.json()
    access_token = token_payload["access_token"]

    pprint(token_payload)

    print("\n****** Introspect Token *************")
    decoded_token = jwt.decode(
        access_token, options={"verify_signature": False}
    )

    pprint(decoded_token)

    token_payload.update({
        "decoded_token": decoded_token
    })
    return token_payload

def cli():
    """
    CLI for running this script
    """
    parser = argparse.ArgumentParser(
        description='Get access token for client'
    )
    parser.add_argument(
        "--client_id",
        default=KEYCLOAK_CLIENT_ID,
        help="Keycloak Client ID",
    )
    parser.add_argument(
        "--client_secret",
        default=KEYCLOAK_CLIENT_SECRET,
        help="Keycloak Client secret",
    )
    parser.add_argument(
        "--issuer",
        default=KEYCLOAK_ISSUER,
        help="Keycloak Issuer URL",
    )
    args = parser.parse_args()

    get_access_token(args.client_id, args.client_secret, args.issuer)

if __name__ == "__main__":
    cli()


import time
import numpy as np
import dolfin as df
import petsc4py as p4py
import reduced_models.vasculature_io as vio
from integrate_2d_surface import integrate_2d_surface, integrate_2d_surface_mat

class NutrientSolver:
    def __init__(self, mesh, kappa) -> None:
        self.function_space = V = df.FunctionSpace(mesh, 'P', 1)

        self.f_2d = df.Function(V, name='rhs')

        self.kappa = kappa

    def assemble_2d_source_terms(self, surface_triangles, accuracy):
        num_triangles = int(len(surface_triangles) / 3)
        surface_values = 1 * np.ones(num_triangles)

        tic = time.perf_counter()
        self.indices_list, self.values_list = integrate_2d_surface(self.function_space, surface_triangles, surface_values, accuracy)
        toc = time.perf_counter()
        print(f"needed {toc - tic:0.4f} seconds to assemble the 2d surface integrals")

        tic = time.perf_counter()
        self.mat_row_indices, self.mat_col_indices, self.mat_values = integrate_2d_surface_mat(self.function_space, surface_triangles, surface_values, accuracy)
        toc = time.perf_counter()
        print(f"needed {toc - tic:0.4f} seconds to assemble the 2d surface integrals")

        f_vec = self.f_2d.vector().vec()
        f_vec.setValues(self.indices_list, self.values_list, p4py.PETSc.InsertMode.ADD)
        f_vec.assemble()

    def save_2d_source_terms(self, path):
        with df.XDMFFile(path) as f:
            f.write_checkpoint(self.f_2d, 'nutrients', 0, df.XDMFFile.Encoding.HDF5, True)

    def load_2d_source_terms(self, path):
        with df.XDMFFile(path) as f:
            f.read_checkpoint(self.f_2d, 'nutrients')

    def zero_2d_source_terms(self):
        self.f_2d.interpolate(df.Constant(0))

    def solve(self, alpha, beta, f):
        u = df.TrialFunction(self.function_space)
        v = df.TestFunction(self.function_space)
        a = self.kappa * df.inner(df.grad(u), df.grad(v)) * df.dx + alpha * u * v * df.dx
        bcs = []

        A = df.assemble(a)
        b = df.assemble(f * v * df.dx)
        df.as_backend_type(b).axpy(beta, self.f_2d.vector())
        print(len(self.mat_row_indices), len(self.mat_col_indices), len(self.mat_values))
        print(np.array(self.mat_row_indices).max(), np.array(self.mat_col_indices).max(), np.array(self.mat_values).max())
        A_mat = df.as_backend_type(A).mat()
        #a = A_mat.getValue(61255, 61255)
        print('min', np.array(self.mat_values).min())
        print('ind: ', self.mat_row_indices[0], self.mat_col_indices[0])
        for (row,col,val) in zip(self.mat_row_indices, self.mat_col_indices, self.mat_values):
            #if row == 61225 and col == 61225:
            #    a += val
            A_mat.setValue(row, col, beta * val, p4py.PETSc.InsertMode.ADD)
        A_mat.assemble()
        #print('end', a, A_mat.getValue(61255, 61255))

        #df.as_backend_type(A).mat().setValuesRCV(self.mat_row_indices, self.mat_col_indices, self.mat_values, p4py.PETSc.InsertMode.ADD)

        for bc in bcs:
            bc.apply(A)
            bc.apply(b)

        u = df.Function(self.function_space, name='nutrients')

        solver = df.PETScKrylovSolver('cg')
        df.PETScOptions.set('ksp_atol', 1e-16)
        df.PETScOptions.set('ksp_rtol', 1e-8)
        df.PETScOptions.set('pc_type', 'hypre')
        df.PETScOptions.set('pc_hypre_type', 'boomeramg')
        solver.set_operator(A)
        solver.set_from_options()

        it = solver.solve(u.vector(), b)
        print(f'solving nutrients needed {it} iterations')

        return u

if __name__ == '__main__':
    N = 64
    M = 4

    vasculature = vio.read_default_vanilla()
    #mesh: df.Mesh = vio.get_bounding_cube(vasculature, N)
    mesh = df.Mesh()
    with df.XDMFFile('data/tetrahedral-meshes/lung.xdmf') as f:
        f.read(mesh)
    mesh = df.refine(mesh)
    mesh = df.refine(mesh)
    mesh = df.refine(mesh)
    # mesh: df.Mesh = vio.get_bounding_cube(vasculature, N)
    print(f'tets: {mesh.num_entities_global(3)}')

    alpha = df.Constant(0.05)
    beta = 1
    kappa = df.Constant(0.01)
    solver = NutrientSolver(mesh, kappa)
    solver.assemble_2d_source_terms(vasculature, M)
    solver.save_2d_source_terms('tmp/nutrients_checkpoint_test.xdmf')
    solver.zero_2d_source_terms()
    solver.load_2d_source_terms('tmp/nutrients_checkpoint_test.xdmf')
    u = solver.solve(alpha=alpha, beta=beta, f=df.Constant(0))

    print(u.vector().norm('l2'))

    file = df.File('output/solution.pvd')
    file << u


import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt

####################################################################################################
######################################## NEURAL NETWORKS ###########################################
####################################################################################################

# for Q6.1.1
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()

        self.main = nn.Sequential(
            nn.Linear(1024, 64),
            nn.Sigmoid(),
            nn.Linear(64, 36),
            #nn.Softmax()
        )

    def forward(self, X):
        return self.main(X)

# for Q6.1.2
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()

        self.conv_layers = nn.Sequential(
            nn.Conv2d(1, 10, kernel_size=5),
            nn.MaxPool2d(2),
            nn.ReLU(),
            nn.Conv2d(10, 20, kernel_size=5),
            nn.Dropout(),
            nn.MaxPool2d(2),
            nn.ReLU()
        )

        self.fc_layers = nn.Sequential(
            nn.Linear(500, 64),
            nn.Sigmoid(),
            nn.Linear(64, 36)
            #nn.Softmax()
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = torch.flatten(x, 1)
        x = self.fc_layers(x)
        return x

# for Q6.1.3
class CNNcifar(nn.Module):
    def __init__(self):
        super(CNNcifar, self).__init__()

        self.conv_layers = nn.Sequential(
            # nn.Conv2d(3, 32, kernel_size=5),
            # nn.MaxPool2d(2, 2),
            # nn.ReLU(),
            # nn.Conv2d(32, 64, kernel_size=5),
            # nn.Dropout(0.2),
            # nn.MaxPool2d(2, 2),
            # nn.ReLU(),
            # nn.BatchNorm2d(64)

            # sees 32x32x3 image tensor
            nn.Conv2d(3, 16, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # sees 16x16x16 tensor
            nn.Conv2d(16, 32, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            # sees 8x8x32 tensor
            nn.Conv2d(32, 64, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)

            # nn.Conv2d(3, 32, 5, padding=1),
            # nn.MaxPool2d(2, 2),
            # nn.Conv2d(32, 64, 5, padding=1),
            # nn.MaxPool2d(2, 2)
        )

        self.fc_layers = nn.Sequential(
            nn.Linear(1024, 500),
            nn.ReLU(),
            nn.Linear(500, 10),
            nn.ReLU()
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = torch.flatten(x, 1)
        x = self.fc_layers(x)
        return x

# for Q6.2
class CNN2(nn.Module):
    def __init__(self):
        super(CNN2, self).__init__()

        self.conv_layers = nn.Sequential(
            nn.Conv2d(3, 10, kernel_size=5),
            nn.MaxPool2d(2),
            nn.ReLU(),
            nn.Conv2d(10, 20, kernel_size=5),
            nn.Dropout(),
            nn.MaxPool2d(2),
            nn.ReLU(),
        )

        self.fc_layers = nn.Sequential(
            nn.Linear(56180, 128),
            nn.Sigmoid(),
            nn.Linear(128, 17),
            #nn.Softmax()
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = torch.flatten(x, 1)
        x = self.fc_layers(x)
        return x

####################################################################################################
######################################## HELPER FUNCTIONS ##########################################
####################################################################################################

# convert numpy array to dataloader
def np2loader(X, y, batchsize=128, shuffling=True):
    y = y.argmax(axis=1)
    X = torch.from_numpy(np.float32(X))
    y = torch.from_numpy(y)
    data = torch.utils.data.TensorDataset(X, y)
    if batchsize != None:
        loader = torch.utils.data.DataLoader(data, batch_size=batchsize, shuffle=shuffling)
    else:
        loader = torch.utils.data.DataLoader(data, shuffle=shuffling)
    return loader

def training_loop(myNet, trainLoader, validLoader, device, max_iters, learning_rate, lossf, optimizer, fname, flatten=True):
    # Training loop
    # Initialize the network
    myNet = myNet.to(device)
    print(myNet)
    train_loss_list, train_acc_list, val_loss_list, val_acc_list = [], [], [], []
    for itr in range(max_iters):
        myNet.train()

        total_loss = 0.0
        total_correct = 0.0
        total_instances = 0

        for times, data in enumerate(trainLoader):
            inputs, labels = data[0].to(device), data[1].to(device)
            if flatten:
                inputs = inputs.view(inputs.shape[0], -1)

            # Zero the parameter gradients
            optimizer.zero_grad()
            # Foward, backward, optimize
            outputs = myNet(inputs)
            loss = lossf(outputs, labels)
            loss.backward()
            optimizer.step()
            total_loss += loss

        train_accuracy, train_loss = evaluate_model(myNet, trainLoader, lossf, device, flatten)
        train_acc_list.append(train_accuracy)
        train_loss_list.append(train_loss)

        val_accuracy, val_loss = evaluate_model(myNet, validLoader, lossf, device, flatten)
        val_acc_list.append(val_accuracy)
        val_loss_list.append(val_loss)

        if itr % 10 == 0:
            print(
                "itr: {:02d} \t loss: {:.2f} \t acc : {:.2f} \t eval_acc : {:.2f}".format(
                    itr, total_loss, train_accuracy, val_accuracy
                )
            )

    # save the weights
    torch.save(myNet.state_dict(), fname)

    # visualize
    plot_train_valid(train_acc_list, val_acc_list, "accuracy")
    plot_train_valid(train_loss_list, val_loss_list, "average loss")
    plot_train(train_acc_list, "accuracy")
    plot_train(train_loss_list, "average loss")

def evaluate_model(myNet, dataLoader, lossf, device, flatten=True, my_class=False):
    myNet.eval()
    total_loss = 0.0
    total_correct = 0.0
    total_instances = 0
    for times, data in enumerate(dataLoader):
        inputs, labels = data[0].to(device), data[1].to(device)
        if my_class != False:
            for i in range(len(labels)):
                labels[i] = my_class
        if flatten:
            inputs = inputs.view(inputs.shape[0], -1)
        outputs = myNet(inputs)
        loss = lossf(outputs, labels)
        # Total loss
        total_loss += loss.item()
        with torch.no_grad():
            # average accuracy
            classifications = torch.argmax(outputs, dim=1)
            correct_predictions = sum(classifications==labels).item()
            total_correct+=correct_predictions
            total_instances+=len(inputs)
    accuracy = round(total_correct/total_instances, 4)
    return accuracy, total_loss/total_instances

# Plot train and valid loss / accuracies
def plot_train_valid(train_data, valid_data, datatype):
    plt.plot(range(len(train_data)), train_data, label="training")
    plt.plot(range(len(valid_data)), valid_data, label="validation")
    plt.xlabel("epoch")
    plt.ylabel(datatype)
    plt.xlim(0, len(train_data) - 1)
    plt.ylim(0, None)
    plt.legend()
    plt.grid()
    plt.show()

# Plot train and loss / accuracies
def plot_train(train_data, datatype):
    plt.plot(range(len(train_data)), train_data, label="training")
    plt.xlabel("epoch")
    plt.ylabel(datatype)
    plt.xlim(0, len(train_data) - 1)
    plt.ylim(0, None)
    plt.legend()
    plt.grid()
    plt.show()


from copy import deepcopy

fn = "input.txt"
# fn = "test.txt"

def part1(data):
    pass

def part2(data):
    pass

class Grid:
    def __init__(self):
        self.g = {}
        self.min_x = 100000
        self.max_x = -100000
        self.min_y = 100000
        self.max_y = -100000
        self.min_z = 100000
        self.max_z = -100000
        self.min_w = 100000
        self.max_w = -100000

    def set(self, x, y, z, w, c):
        # if c == '#':
        self.min_x = min(self.min_x, x-1)
        self.min_y = min(self.min_y, y-1)
        self.min_z = min(self.min_z, z-1)
        self.min_w = min(self.min_w, w-1)

        self.max_x = max(self.max_x, x+1)
        self.max_y = max(self.max_y, y+1)
        self.max_z = max(self.max_z, z+1)
        self.max_w = max(self.max_w, w+1)
        self.g[x,y,z, w] = c

    def get(self, x, y, z, w):
        try:
            return self.g[x, y, z, w]
        except KeyError:
            return '.'

    def neighbors(self, x, y, z, w):
        cells = []
        for u in range(-1, 2):
            for v in range(-1, 2):
                for wz in range(-1, 2):
                    for ww in range(-1, 2):
                        if (u,v,wz,ww) != (0, 0, 0, 0):
                            x1 = x + u
                            y1 = y + v
                            z1 = z + wz
                            w1 = w + ww
                            c = self.get(x1, y1, z1, w1)
                            if c == '#':
                                cells.append(c)
        return cells

    def iter(self):
        for w in range(self.min_w, self.max_w+1):
            for z in range(self.min_z, self.max_z+1):
                for y in range(self.min_y, self.max_y+1):
                    for x in range(self.min_x, self.max_x+1):
                        yield x, y, z, w

    def print(self):
        for z in range(self.min_z, self.max_z):
            print('\nz=', z-1000)
            for y in range(self.min_y, self.max_y):
                line = []
                for x in range(self.min_x, self.max_x):
                    line.append(self.get(x, y, z))
                print(''.join(line))

# parse the data
grid = Grid()
z = 0
w = 0
for y, line in enumerate(open(fn)):
    for x, c in enumerate(line):
        grid.set(x+1000, y+1000, z+1000, w+1000, c)

for i in range(6):
    newgrid = deepcopy(grid)
    for (x, y, z, w) in grid.iter():
        c = grid.get(x, y, z, w)
        n = grid.neighbors(x, y, z, w)
        if c=='#':
            if 2 <= n.count('#') <= 3:
                newcell = '#'
            else:
                newcell = '.'
        else:
            if n.count('#') == 3:
                newcell = '#'
            else:
                newcell = '.'
        newgrid.set(x, y, z, w, newcell)

    grid = newgrid
    # print('-----------------')
    # grid.print()

count = 0
for x, y, z, w in grid.iter():
    count += grid.get(x, y, z, w) == '#'
print(count)


import sys
import pathlib
import json

def get_settings_dir(name) -> pathlib.Path:
    """
    Returns a parent directory path
    where persistent application data can be stored.

    # linux: ~/.local/share
    # macOS: ~/Library/Application Support
    # windows: C:/Users/<USER>/AppData/Roaming
    """

    home = pathlib.Path.home()

    if sys.platform == "win32":
        config_path = home / "AppData/Roaming"
    elif sys.platform == "linux":
        config_path = home / ".local/share"
    elif sys.platform == "darwin":
        config_path = home / "Library/Application Support"
    else:
        raise OSError("OS not supported")

    config_path = config_path / name / "config.json"
    return config_path

def create_dir(settings_file):
    try:
        settings_file.parent.mkdir(parents=True)
    except FileExistsError:
        pass

class Settings(object):
    def __init__(self, name):
        self._settings = {
            "jira_domain": "arcanys.atlassian.net",
            "jira_username": None,
            "jira_token": None,
            "tempo_token": None,
        }
        self._file = get_settings_dir(name)

    def load(self):
        create_dir(self._file)
        if pathlib.Path.is_file(self._file):
            with open(self._file, "r") as fp:
                data = json.load(fp)
            for k, v in data.items():
                if k in self._settings:
                    self._settings[k] = v

    def save(self):
        create_dir(self._file)
        with open(self._file, "w") as fp:
            json.dump(self._settings, fp)

    def get(self, key):
        return self._settings.get(key)

    def set(self, key, value):
        self._settings[key] = value


from databases import Database
from sqlalchemy import select, func
from typing import Dict

from db import models
from schemas import companies_members as schema_cm
from utils.exceptions import MyExceptions

class GenericDatabase:

    def __init__(self, db: Database):
        self.db = db
        self.company_members_table = models.company_members
        self.company_table = models.companies
        self.users_table = models.users
        self.quiz_table = models.quiz
        self.question_table = models.question
        self.avarage_mark_table=models.avarage_mark
        self.uiz_result_table = models.companies
        self.quiz_result_table=models.quiz_result
        self.exception = MyExceptions

    async def insert_values(self, table, values: Dict):
        return await self.db.execute(table.insert().values(values))

    async def company_members_join_company(self):
        query_joins = self.company_members_table.join(self.company_table)
        query = select(
            self.company_table.c.id.label('company_id'),
            self.company_table.c.name.label('company_name'),
            self.company_members_table.c.is_company_admin,
            self.company_members_table.c.active_member.label('active_from'),
            ).select_from(query_joins)

        return query


from flask import current_app
from sqlalchemy import asc, desc
from sqlalchemy.exc import SQLAlchemyError

from app import db
from app.businesses.exceptions import EntityNotFoundException
from app.businesses.models import Business, Category, Tag, BusinessUpload

CONTAINS = '%{}%'

class BaseRepository(object):
    model = None
    _session = None

    def __init__(self):
        self._session = db.session

    @property
    def session(self):
        return self._session or current_app.session()

    @property
    def query(self):
        assert self.model, "A model is required to use the query property."
        return self.session.query(self.model)

    def get(self, id, strict=False):
        entity = self.query.get(id)
        if strict and not entity:
            raise EntityNotFoundException
        return entity

    def filter(self, **kwargs):
        query = self.query.filter_by(**kwargs)
        return query

    def save(self, entity):
        self.session.add(entity)
        try:
            self.session.commit()
        except SQLAlchemyError:
            self.session.rollback()
            raise
        return entity

    def save_many(self, entities):
        self.session.add_all(entities)
        self.session.commit()
        return entities

    def update(self, id_, **kwargs):
        db_entity = self.get(id_, strict=True)
        self._update_fields(db_entity, **kwargs)
        self.session.commit()
        return db_entity

    @classmethod
    def _update_fields(cls, db_entity, **kwargs):
        for key, value in kwargs.items():
            setattr(db_entity, key, value)

    def _delete(self, id_):
        try:
            db_entity = self.get(id_, strict=True)
            self.session.delete(db_entity)
            self.session.commit()
        except EntityNotFoundException:
            raise

class BusinessRepository(BaseRepository):
    model = Business

    def save(self, entity):
        entity = self.save_tags(entity)
        super(BusinessRepository, self).save(entity)
        return entity

    def save_tags(self, entity):
        tag_repository = TagRepository()
        existing_tag = {t.name: t for t in tag_repository.query.all()}
        tag_to_be_add = []
        for t in entity.tags:
            if t.name in existing_tag:
                tag_to_be_add.append(existing_tag[t.name])
            else:
                tag_to_be_add.append(t)
        entity.tags = tag_to_be_add
        return entity

    def filter(self, id=None, querySearch=None, accepted_at=None, status=None, order=None, order_by=None,
               exclude_deleted=None,
               **kwargs):
        query = self.query

        if exclude_deleted:
            query = query.filter(Business.deleted_at.is_(None))

        if id:
            return query.filter(Business.id == id)

        if querySearch:
            querySearch = CONTAINS.format(querySearch)
            query = query.filter(Business.name.ilike(querySearch))

        if accepted_at:
            query = query.filter(Business.accepted_at == accepted_at)

        if status:
            for s in status:
                query = query.filter(Business.status == s)

        if order_by:
            order = asc if order == "ASC" else desc
            if order_by == "name":
                query = query.order_by(order(Business.name))

        return query

    def delete(self, id):
        business = self.get(id=id)
        business.delete()
        self.save(business)

class CategoryRepository(BaseRepository):
    model = Category

    def save(self, entity):
        super(CategoryRepository, self).save(entity)
        return entity

    def filter(self, *args, **kwargs):
        query = super(CategoryRepository, self).filter(**kwargs)

        return query

    def delete(self, id_):
        self._delete(id_)

class TagRepository(BaseRepository):
    model = Tag

    def save(self, entity):
        super(TagRepository, self).save(entity)
        return entity

    def filter(self, *args, **kwargs):
        query = super(TagRepository, self).filter(**kwargs)

        return query

    def delete(self, id_):
        self._delete(id_)

    def get_tags_with_id(self, new_tags):
        existing_tag_names = {t.name: t for t in self.query.all()}
        tags_to_be_add = []
        for t in new_tags:
            if t.name in existing_tag_names:
                tags_to_be_add.append(existing_tag_names[t.name])
            else:
                tags_to_be_add.append(t)
        return tags_to_be_add

class BusinessUploadRepository(BaseRepository):
    model = BusinessUpload


from sqlalchemy.engine import Connection

from saltapi.repository.finder_chart_repository import FinderChartRepository
from saltapi.service.finder_chart_service import FinderChartService

def test_get_finder_chart(db_connection: Connection) -> None:
    finding_chart_id = 55345
    expected_proposal_code = "2015-2-SCI-028"
    expected_finder_chart_path = (
        expected_proposal_code + "/4/Included/FindingChart_1445723219288.pdf"
    )

    repository = FinderChartRepository(db_connection)
    service = FinderChartService(repository)

    proposal_code, finding_chart_path = service.get_finder_chart(
        f"{finding_chart_id}.pdf"
    )

    assert expected_proposal_code == proposal_code
    assert expected_finder_chart_path in str(finding_chart_path)


import numpy as np
import pandas as pd

class AdalineSGD(object):

    def __init__(self, eta, n_iter, shuffle=True, random_state=1):
        self.eta = eta
        self.n_iter = n_iter
        self.shuffle = shuffle
        self.is_initialized = False
        self.random_state = random_state

    def fit(self, x, y):
        def fit(self, x, y):
            rgen = np.random.RandomState(self.random_state)
            self.w_ = rgen.normal(loc=0.0, scale=0.1, size=x.shape[1] + 1)
            self.cost_ = []
            for i in range(self.n_iter):
                output = self.activation(self.n_input(x))
                error = y - output
                self.w[1:] = self.eta * x.T.dot(error)
                self.w_[0] = self.eta * error
                cost = (-y.dot(np.log(output)) - (1 - y).dot(np.log(1 - output)))
                self.cost_.append(cost)
            return self

        return self

    def activation(self, x):
        return 1 / (1 - np.exp(-np.clip(x, -250, 250)))

    def n_input(self, x):
        return np.dot(x, self.w_[1:]) + self.w_[0]

    def predict(self, x):
        return np.where(self.activation(self.n_input(x)) >= 0.0, 1, 0)

data = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)
x = data.iloc[:100, [0, 2]].values
target = data.iloc[:100, 4].values
y = np.where(target == 'Iris-setosa', 0, 1)
logistinc_regression = AdalineSGD(eta=0.1, n_iter=10)


def search():
    w = input("请输入要查询的单词:")
    fr = open("test.txt", 'r')
    dic = {}
    for line in fr.readlines():
        line = line.replace("\n", "")
        line = list(line.split(","))
        key = line[0]
        coment = line[1:]
        dic[key] = coment  #
    if w in dic.keys():
        print(dic[w])
    else:
        print("你要查询的单词不存在！")
    fr.close()
def add():
    ww = input("请输入你要添加的单词：")
    flag = 0
    dic = {}
    f = open("test.txt", 'r')
    for line in f.readlines():
        line = line.replace("\n", "")
        line = list(line.split(","))
        key = line[0]
        coment = line[1:]
        dic[key] = coment
        if ww in dic.keys():
            f.close()
            flag = 1
            print("输入的单词已经存在!")
            break
        else:
            f.close()
    if flag != 1:  # 如果输入的单词不存在，则进行汉语意思的输入，若有多个意思，则用英文逗号隔开
        fw = open("test.txt", 'a')
        mean = input("若有多个意思，则用英文逗号隔开:")
        fw.write(ww + ',' + mean + '\r\n')
        fw.close()
while True:
    str = input("您要查询、添加还是退出字典:")
    if str =="查询":
        search()
        continue
    elif str == "添加":
        add()
        continue
    elif str == "退出字典":
        print("程序已经退出!")
        break


# pylint: disable=protected-access

from asyncio import create_task
from unittest.mock import AsyncMock, Mock, call, patch
from unittest.async_case import IsolatedAsyncioTestCase

from faker import Faker
from fetcher import Fetcher

class TestFetcher(IsolatedAsyncioTestCase):
    def test_parse_url(self):
        test_cases = [
            {
                "text": "word, ..'word'', qwerty %(QWERTY something!@ +)WORD&(@%",  # noqa
                "expected_result": {"word": 3, "qwerty": 2, "something": 1},
            },
            {"text": "", "expected_result": {}},
            {
                "text": "word qwerty something",
                "expected_result": {"word": 1, "qwerty": 1, "something": 1},
            },
        ]

        with patch("fetcher.BeautifulSoup") as bs_mock:
            bs_instance = bs_mock.return_value
            for case in test_cases:
                bs_instance.get_text.return_value = case["text"]

                fetcher = Fetcher(1, 4)
                result = fetcher.parse_url("")

                self.assertEqual(case["expected_result"], result)

    async def test_fetch_url(self):
        fake = Faker()
        urls = [fake.url() for _ in range(5)]
        page_text = fake.text()

        fetcher = Fetcher()

        for url in urls:
            await fetcher._queue.put(url)

        session_mock = Mock()
        resp_mock = AsyncMock()
        session_mock.get.return_value = resp_mock
        resp_mock.__aenter__.return_value.text.return_value = page_text
        callback_mock = Mock()

        task = create_task(fetcher._fetch_url(session_mock, callback_mock))
        await fetcher._queue.join()
        task.cancel()

        self.assertTrue(fetcher._queue.empty())

        expected_result = fetcher.parse_url(page_text)
        self.assertEqual(callback_mock.call_count, len(urls))
        self.assertEqual(
            callback_mock.mock_calls, [call(expected_result)] * len(urls)
        )

    async def test_start(self):
        fetcher = Fetcher(workers_count=5)
        fetcher.start(Mock())

        for worker in fetcher._workers:
            self.assertFalse(worker.done())

    async def test_fetch(self):
        fake = Faker()
        urls = [fake.url() for _ in range(10)]

        fetcher = Fetcher(workers_count=5, max_size=11)
        await fetcher.fetch(urls)

        for url in urls:
            self.assertEqual(await fetcher._queue.get(), url)
            fetcher._queue.task_done()

        fetcher.stop()


"""
Parse text from PDF file.

Approach: Read PDF into list, then search list for certain terms. Append lines
where terms are found to new file or list, then parse those lines for the info

394.22
26572588200001
1503541657
"""

def getTotDue(x):
	return float(''.join(num for num in x if num.isdigit() or num == '.'))

def getAcctInvNmbr(x):
    return int(''.join(num for num in x if num.isdigit()))

def main(pdfName,filename,totalDue,accountNumber,invoiceNumber):
    pdfFileObj = open('C:\\Users\\tweatherall\\Desktop\\PythonScripts\\Input_Output_Test_Files\\' + pdfName,'rb')
    pdfReader = PyPDF2.PdfFileReader(pdfFileObj)

    pageObj = pdfReader.getPage(0)
    text = pageObj.extractText()
    ls = [item.strip() for item in list(text.split('\n'))]

    accountNum = []
    invNum = []
    totDue = []
    for item in ls:
        if 'account number'.upper() in item.upper():
            #print(item)
            #accountNum.append(item)
            accountNum += item.split()
        if 'invoice number'.upper() in item.upper():
            #print(item)
            #invNum.append(item)
            invNum += item.split()
        if 'total current charges'.upper() in item.upper():
            #totDue.append(item.split())
            totDue += item.split()

    nmbr = [] #holds list of numbers in totDue list
    for item in totDue:
        try:
            nmbr.append(getTotDue(item))
        except:
            continue

    AcctNmbr = [] #holds list of numbers in accountNum list
    for account in accountNum:
        try:
            AcctNmbr.append(getAcctInvNmbr(account))
        except:
            continue

    InvNmbr = [] #holds list of numbers in invNum list
    for invoice in invNum:
        try:
            InvNmbr.append(getAcctInvNmbr(invoice))
        except:
            continue

    with open('C:\\Users\\tweatherall\\Desktop\\PythonScripts\\Input_Output_Test_Files\\' + filename,'w') as out_file:
        #try:
        print('Writing file...')
        for item in nmbr:
            if item == float(totalDue):
                print(item)
                out_file.write('Total Due: ')
                out_file.write(str(item))
                out_file.write('\n')
        for item in AcctNmbr:
            if item == int(accountNumber):
                print(item)
                out_file.write('Account Number: ' + str(item))
                out_file.write('\n')
        for item in InvNmbr:
            if item == int(invoiceNumber):
                print(item)
                out_file.write('Invoice Number: ' + str(item))
                out_file.write('\n')
        # except:
        #     print('EOF reached')

if __name__ == "__main__":
    import PyPDF2
    import re
    pdfFile = input('Enter PDF to parse:')
    outFile = input('Enter output file name:')
    totalDue = input('Enter total due amount:')
    accountNumber = input('Enter account number:')
    invoiceNumber = input('Enter invoice number:')
    main(pdfFile,outFile,totalDue,accountNumber,invoiceNumber)