polyfix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. polyfix-0.1.0/PKG-INFO +36 -0
  2. polyfix-0.1.0/README.md +6 -0
  3. polyfix-0.1.0/pyproject.toml +42 -0
  4. polyfix-0.1.0/src/polyfix/__init__.py +1 -0
  5. polyfix-0.1.0/src/polyfix/bends/bends.py +130 -0
  6. polyfix-0.1.0/src/polyfix/bends/errors.py +61 -0
  7. polyfix-0.1.0/src/polyfix/bends/graph.py +195 -0
  8. polyfix-0.1.0/src/polyfix/bends/interfaces.py +363 -0
  9. polyfix-0.1.0/src/polyfix/bends/main.py +147 -0
  10. polyfix-0.1.0/src/polyfix/bends/points.py +89 -0
  11. polyfix-0.1.0/src/polyfix/bends/utils.py +64 -0
  12. polyfix-0.1.0/src/polyfix/bends/viz.py +79 -0
  13. polyfix-0.1.0/src/polyfix/cli/main.py +26 -0
  14. polyfix-0.1.0/src/polyfix/cli/make/main.py +96 -0
  15. polyfix-0.1.0/src/polyfix/cli/make/utils.py +53 -0
  16. polyfix-0.1.0/src/polyfix/cli/studies/main.py +42 -0
  17. polyfix-0.1.0/src/polyfix/cli/studies/study_msd.py +177 -0
  18. polyfix-0.1.0/src/polyfix/cli/studies/study_validation.py +33 -0
  19. polyfix-0.1.0/src/polyfix/config.py +3 -0
  20. polyfix-0.1.0/src/polyfix/examples/bends.py +111 -0
  21. polyfix-0.1.0/src/polyfix/examples/domains.py +56 -0
  22. polyfix-0.1.0/src/polyfix/examples/layout.py +43 -0
  23. polyfix-0.1.0/src/polyfix/examples/msd.py +105 -0
  24. polyfix-0.1.0/src/polyfix/examples/sample_updates.py +17 -0
  25. polyfix-0.1.0/src/polyfix/geometry/layout.py +69 -0
  26. polyfix-0.1.0/src/polyfix/geometry/modify/delete.py +15 -0
  27. polyfix-0.1.0/src/polyfix/geometry/modify/precision.py +76 -0
  28. polyfix-0.1.0/src/polyfix/geometry/modify/update.py +158 -0
  29. polyfix-0.1.0/src/polyfix/geometry/modify/validate.py +182 -0
  30. polyfix-0.1.0/src/polyfix/geometry/ortho.py +128 -0
  31. polyfix-0.1.0/src/polyfix/geometry/paired_coords.py +61 -0
  32. polyfix-0.1.0/src/polyfix/geometry/range.py +93 -0
  33. polyfix-0.1.0/src/polyfix/geometry/shapely_helpers.py +17 -0
  34. polyfix-0.1.0/src/polyfix/geometry/surfaces.py +164 -0
  35. polyfix-0.1.0/src/polyfix/geometry/vectors.py +206 -0
  36. polyfix-0.1.0/src/polyfix/layout/interfaces.py +78 -0
  37. polyfix-0.1.0/src/polyfix/layout/main/move.py +143 -0
  38. polyfix-0.1.0/src/polyfix/layout/main/plan.py +103 -0
  39. polyfix-0.1.0/src/polyfix/layout/neighbors.py +138 -0
  40. polyfix-0.1.0/src/polyfix/layout/viz.py +103 -0
  41. polyfix-0.1.0/src/polyfix/nonortho/dot.py +49 -0
  42. polyfix-0.1.0/src/polyfix/nonortho/interfaces.py +67 -0
  43. polyfix-0.1.0/src/polyfix/nonortho/main.py +28 -0
  44. polyfix-0.1.0/src/polyfix/nonortho/rotation.py +25 -0
  45. polyfix-0.1.0/src/polyfix/paths.py +13 -0
  46. polyfix-0.1.0/src/polyfix/pydantic_models.py +108 -0
  47. polyfix-0.1.0/src/polyfix/rotate/main.py +21 -0
  48. polyfix-0.1.0/src/polyfix/rotate/utils.py +61 -0
  49. polyfix-0.1.0/src/polyfix/visuals/styles.py +83 -0
  50. polyfix-0.1.0/src/polyfix/visuals/visuals.py +146 -0
polyfix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.3
2
+ Name: polyfix
3
+ Version: 0.1.0
4
+ Summary: Module for creating EnergyPlus ready floor plans from geometry containing holes.
5
+ Author: Juliet Nwagwu Ume-Ezeoke
6
+ Author-email: Juliet Nwagwu Ume-Ezeoke <jnwagwu@stanford.edu>
7
+ Requires-Dist: bpython>=0.25
8
+ Requires-Dist: cyclopts>=4.4.3
9
+ Requires-Dist: expression>=5.6.0
10
+ Requires-Dist: geom>=0.3.0
11
+ Requires-Dist: loguru>=0.7.3
12
+ Requires-Dist: matplotlib>=3.10.6
13
+ Requires-Dist: networkx>=3.5
14
+ Requires-Dist: numpy>=2.3.3
15
+ Requires-Dist: pipe>=2.2
16
+ Requires-Dist: pre-commit>=4.5.1
17
+ Requires-Dist: ptpython>=3.0.31
18
+ Requires-Dist: pydantic>=2.12.5
19
+ Requires-Dist: pyprojroot>=0.3.0
20
+ Requires-Dist: pytest>=8.4.2
21
+ Requires-Dist: pytest-cov>=7.0.0
22
+ Requires-Dist: shapely>=2.1.2
23
+ Requires-Dist: snakemake>=9.14.5
24
+ Requires-Dist: tomli-w>=1.2.0
25
+ Requires-Dist: types-networkx>=3.5.0.20251106
26
+ Requires-Dist: utils4plans
27
+ Requires-Dist: whenever>=0.9.3
28
+ Requires-Python: >=3.13
29
+ Description-Content-Type: text/markdown
30
+
31
+ # polyfix
32
+
33
+ ### Geometry conventions
34
+
35
+ - Polygonal domains can be specified using coordinate in either a clockwise or counterclockwise direction.
36
+ - After the coordinates are normalized, they will have a clockwise direction, starting from the bottom left. This comes from Shapely's defaults for normalizing
@@ -0,0 +1,6 @@
1
+ # polyfix
2
+
3
+ ### Geometry conventions
4
+
5
+ - Polygonal domains can be specified using coordinate in either a clockwise or counterclockwise direction.
6
+ - After the coordinates are normalized, they will have a clockwise direction, starting from the bottom left. This comes from Shapely's defaults for normalizing
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "polyfix"
3
+ version = "0.1.0"
4
+ description = "Module for creating EnergyPlus ready floor plans from geometry containing holes."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Juliet Nwagwu Ume-Ezeoke", email = "jnwagwu@stanford.edu" },
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "bpython>=0.25",
12
+ "cyclopts>=4.4.3",
13
+ "expression>=5.6.0",
14
+ "geom>=0.3.0",
15
+ "loguru>=0.7.3",
16
+ "matplotlib>=3.10.6",
17
+ "networkx>=3.5",
18
+ "numpy>=2.3.3",
19
+ "pipe>=2.2",
20
+ "pre-commit>=4.5.1",
21
+ "ptpython>=3.0.31",
22
+ "pydantic>=2.12.5",
23
+ "pyprojroot>=0.3.0",
24
+ "pytest>=8.4.2",
25
+ "pytest-cov>=7.0.0",
26
+ "shapely>=2.1.2",
27
+ "snakemake>=9.14.5",
28
+ "tomli-w>=1.2.0",
29
+ "types-networkx>=3.5.0.20251106",
30
+ "utils4plans",
31
+ "whenever>=0.9.3",
32
+ ]
33
+
34
+ [project.scripts]
35
+ polyfix = "polyfix.cli.main:main"
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.8.3,<0.9.0"]
39
+ build-backend = "uv_build"
40
+
41
+ # [tool.uv.sources]
42
+ # utils4plans = { path = "../utils4plans" }
@@ -0,0 +1 @@
1
+ from rich.pretty import pretty_repr
@@ -0,0 +1,130 @@
1
+ from loguru import logger
2
+ from utils4plans.lists import chain_flatten
3
+ from polyfix.geometry.modify.validate import InvalidPolygonError, validate_polygon
4
+ from polyfix.bends.graph import (
5
+ create_surface_graph_for_domain,
6
+ find_small_node_groups,
7
+ get_nodes_data,
8
+ get_successor_node,
9
+ handle_components,
10
+ update_small_nbs,
11
+ )
12
+ from polyfix.geometry.ortho import FancyOrthoDomain
13
+ from polyfix.bends.interfaces import (
14
+ BendHolder,
15
+ KappaOne,
16
+ KappaTwo,
17
+ PiOne,
18
+ PiThree,
19
+ PiTwo,
20
+ )
21
+ import networkx as nx
22
+
23
+ from polyfix.geometry.surfaces import Surface
24
+
25
+
26
+ def check_is_pi_two(G: nx.DiGraph, node: str):
27
+ data = get_nodes_data(G, node)
28
+ if data.is_small and data.is_nb2_small and not data.is_nb_small:
29
+ return True
30
+
31
+
32
+ def identify_pi_twos(domain: FancyOrthoDomain, G_: nx.DiGraph):
33
+
34
+ def make(node: str):
35
+ n1 = get_successor_node(G, node)
36
+ n2 = get_successor_node(G, n1)
37
+
38
+ s1 = get_nodes_data(G, node).surface
39
+ s2 = get_nodes_data(G, n2).surface
40
+ assert s1 and s2
41
+ return PiTwo.from_surfaces(domain, G, s1, s2)
42
+
43
+ G = update_small_nbs(G_)
44
+
45
+ bends = [make(node) for node in G.nodes if check_is_pi_two(G, node)]
46
+
47
+ # only want those where are passing bend checks
48
+ passing_bends = [i for i in bends if i.are_vectors_correct]
49
+ return passing_bends
50
+
51
+
52
+ def is_part_of_pi_twos(bends: list[PiTwo], surface: Surface):
53
+ pi2surfs = chain_flatten([[i.s1, i.s2] for i in bends])
54
+ if surface in pi2surfs:
55
+ return True
56
+
57
+
58
+ def assign_bends(domain: FancyOrthoDomain, domain_name: int | str = ""):
59
+
60
+ bh = BendHolder()
61
+ try:
62
+ validate_polygon(domain.polygon, domain.name)
63
+ except InvalidPolygonError as e:
64
+ logger.error(
65
+ f"Could not validate polygon, and could not assign bends for domain<{domain_name}> ----- {e.message()}"
66
+ )
67
+ return bh
68
+
69
+ G = create_surface_graph_for_domain(domain)
70
+ bh.pi2s.extend(identify_pi_twos(domain, G))
71
+ # TODO find 2pi groups
72
+ components = find_small_node_groups(G)
73
+ logger.trace(f"components = {components}")
74
+
75
+ # TODO:pi2s
76
+
77
+ info = (domain, G)
78
+
79
+ # large_groups = []
80
+ # uncategorized = []H
81
+
82
+ for comp_ in components:
83
+ comp = list(comp_) # TODO: can simplify this.
84
+ size = len(comp)
85
+ if size > 3:
86
+ bh.large.append(comp)
87
+ elif size == 3:
88
+ res = handle_components(G, comp)
89
+ # logger.debug(
90
+ # f"after have handled components: {[i.name_w_domain for i in res]}"
91
+ # )
92
+ bend = PiThree.from_surfaces(*info, *res)
93
+ if bend.are_vectors_correct:
94
+ bh.pi3s.append(bend)
95
+ else:
96
+ bh.large.append(comp)
97
+ # todo: check if vectors are correct, if not goes to same treatment as large..
98
+ #
99
+ elif size == 2:
100
+ res = handle_components(G, comp)
101
+ res = KappaTwo.from_surfaces(*info, *res)
102
+ bh.kappa2s.append(res)
103
+ elif size == 1:
104
+ res = handle_components(G, comp)[0]
105
+ if is_part_of_pi_twos(bh.pi2s, res):
106
+ continue
107
+
108
+ bend = KappaOne.from_surfaces(*info, res)
109
+ if bend.are_vectors_correct:
110
+ bh.kappas.append(bend)
111
+ continue
112
+
113
+ bend = PiOne.from_surfaces(*info, res)
114
+ if bend.are_vectors_correct:
115
+ bh.pis.append(bend)
116
+ continue
117
+
118
+ bh.not_found.append(comp)
119
+
120
+ # bh larges and bad pi3s become kappa2s
121
+ for comp in bh.large:
122
+ res = handle_components(G, comp)
123
+ s1, s2, *_ = res
124
+ bend = KappaTwo.from_surfaces(*info, s1, s2)
125
+ bh.kappa2s.append(bend)
126
+
127
+ return bh
128
+
129
+
130
+ # TODO: consider making the checks of the bend vectos external.. ?
@@ -0,0 +1,61 @@
1
+ from polyfix.geometry.ortho import FancyOrthoDomain
2
+
3
+ from polyfix.bends.interfaces import Bend, BendHolder
4
+ from polyfix.geometry.surfaces import Surface
5
+ from typing import Literal
6
+
7
+ from loguru import logger
8
+
9
+ FAIL_TYPES = Literal[
10
+ "Invalid Move",
11
+ "Problem Finding Bends",
12
+ "Failed to Clean Domain Correctly",
13
+ "Invalid Incoming Domain",
14
+ "Exceeded number of iterations",
15
+ ]
16
+
17
+
18
+ class DomainCleanFailure(Exception):
19
+ def __init__(
20
+ self,
21
+ domain: FancyOrthoDomain,
22
+ fail_type: FAIL_TYPES,
23
+ details: str,
24
+ surfaces: list[Surface] = [],
25
+ bends: BendHolder | None = None,
26
+ current_bend: Bend | None = None,
27
+ ):
28
+ self.domain = domain
29
+ self.fail_type = fail_type
30
+ self.details = details
31
+ self.surfaces = surfaces
32
+ self.bends = bends
33
+ self.current_bend = current_bend
34
+
35
+ def __rich_repr__(self):
36
+ yield "domain", self.domain.name
37
+ yield "fail_type", self.fail_type
38
+ yield "details", self.details
39
+
40
+ def show_message(self, layout_id: str):
41
+ logger.warning(f"[red bold]{self.fail_type} for {layout_id}-{self.domain.name}")
42
+ logger.warning(f"{self.details}")
43
+
44
+ if self.bends:
45
+ logger.warning(self.bends.summary_str)
46
+
47
+ if self.current_bend:
48
+ logger.warning(f"Current bend is {str(self.current_bend)}")
49
+ logger.warning(self.current_bend.study_vectors())
50
+
51
+
52
+ class DomainCleanIterationFailure(Exception):
53
+ def __init__(
54
+ self, domain_name: str, fail_type: FAIL_TYPES, current_bend: Bend | None = None
55
+ ):
56
+ self.domain = domain_name
57
+ self.fail_type = fail_type
58
+ self.current_bend = current_bend
59
+
60
+ def message(self):
61
+ logger.warning(f"[red bold]{self.fail_type} for {self.domain}")
@@ -0,0 +1,195 @@
1
+ from copy import deepcopy
2
+ from dataclasses import dataclass
3
+ from utils4plans.lists import get_unique_one
4
+ from utils4plans.sets import set_difference
5
+
6
+
7
+ from loguru import logger
8
+ from rich.pretty import pretty_repr
9
+ from polyfix.geometry.surfaces import Surface
10
+ from polyfix.geometry.ortho import FancyOrthoDomain
11
+ from typing import Iterable, TypeVar
12
+ import networkx as nx
13
+ from utils4plans.lists import pairwise
14
+ from polyfix.bends.utils import make_repr_obj
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ @dataclass
20
+ class DomainGraph:
21
+ graph: nx.DiGraph
22
+
23
+
24
+ @dataclass
25
+ class NodeData:
26
+ is_small: bool = False
27
+ surface: Surface | None = None
28
+ is_nb_small: bool = False
29
+ is_nb2_small: bool = False
30
+
31
+ @property
32
+ def repr_dict(self):
33
+ def fx():
34
+ yield "surface_name", self.surface.name_w_domain if self.surface else ""
35
+ yield "is_small", self.is_small
36
+ yield "is_nb_small", self.is_nb_small
37
+ yield "is_nb2_small", self.is_nb2_small
38
+
39
+ return make_repr_obj(fx)
40
+
41
+ # def __repr__(self):
42
+ # def fx():
43
+ # yield "is_small", self.is_small
44
+ # yield "surface_name", self.surface.name if self.surface else ""
45
+ #
46
+ # return make_repr_obj(fx, self)
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class SurfaceNode:
51
+ data: NodeData
52
+
53
+
54
+ def get_nodes_data(G: nx.DiGraph, node: str):
55
+ data = G.nodes[node].get("data")
56
+ assert isinstance(data, NodeData)
57
+ d: NodeData = data
58
+ return d
59
+
60
+
61
+ def get_predecesor(G: nx.DiGraph, node: object):
62
+ res = list(G.predecessors(node))[0]
63
+ return get_surface(G, res)
64
+
65
+
66
+ def get_successor(G: nx.DiGraph, node: object):
67
+ res = list(G.successors(node))[0]
68
+ return get_surface(G, res)
69
+
70
+
71
+ def get_successor_node(G: nx.DiGraph, node: object):
72
+ res = list(G.successors(node))
73
+ assert len(res) == 1, f"Expected one successor, but got {res}"
74
+ # should assert num of succesors = 1
75
+ return res[0]
76
+
77
+
78
+ def create_cycle_graph(nodes_: Iterable[T]):
79
+ nodes = list(nodes_)
80
+ node_cycle = nodes + [nodes[0]]
81
+
82
+ # G: nx.DiGraph[SurfaceNode] = nx.DiGraph()
83
+ G = nx.DiGraph()
84
+ for i, j in pairwise(node_cycle):
85
+ G.add_edge(i, j)
86
+
87
+ return G
88
+
89
+
90
+ def create_surface_graph_for_domain(domain: FancyOrthoDomain):
91
+
92
+ surf_names = [i.name_w_domain for i in domain.surfaces]
93
+ G = create_cycle_graph(surf_names)
94
+ node_data = {
95
+ name: {"data": NodeData(is_small=surf.is_small, surface=surf)}
96
+ for name, surf in zip(surf_names, domain.surfaces)
97
+ }
98
+ nx.set_node_attributes(G, node_data)
99
+ return G
100
+
101
+
102
+ def update_small_nbs(G_: nx.DiGraph):
103
+ def check_nb_is_small(G: nx.DiGraph, nb: str):
104
+ data = get_nodes_data(G, nb)
105
+ return data.is_small
106
+
107
+ def update(G: nx.DiGraph, node: str):
108
+ data = get_nodes_data(G, node)
109
+ n1 = get_successor_node(G, node)
110
+ data.is_nb_small = check_nb_is_small(G, n1)
111
+
112
+ n2 = get_successor_node(G, n1)
113
+ data.is_nb2_small = check_nb_is_small(G, n2)
114
+
115
+ G = deepcopy(G_)
116
+ for node in G.nodes:
117
+ update(G, node)
118
+
119
+ return G
120
+
121
+
122
+ def find_small_node_groups(G: nx.DiGraph):
123
+ nodes = [n for n, d in G.nodes(data=True) if d["data"].is_small]
124
+ sg = G.subgraph(nodes)
125
+ components = nx.connected_components(sg.to_undirected())
126
+
127
+ return list(components)
128
+
129
+
130
+ def find_ends_of_a_directed_graph(G: nx.DiGraph):
131
+ root = get_unique_one(G.nodes, lambda x: len(list(G.predecessors(x))) == 0)
132
+ end = get_unique_one(G.nodes, lambda x: len(list(G.successors(x))) == 0)
133
+ return root, end
134
+
135
+
136
+ def order_nodes_based_on_graph(G: nx.DiGraph, nodes_: Iterable[T]):
137
+ nodes = list(nodes_)
138
+ nodes_to_remove = set_difference(G.nodes, nodes)
139
+ sg = deepcopy(G)
140
+ sg.remove_nodes_from(nodes_to_remove)
141
+
142
+ logger.trace(sg.nodes)
143
+ if len(nodes) > 2:
144
+ root, end = find_ends_of_a_directed_graph(sg)
145
+ paths = list(nx.shortest_simple_paths(sg, root, end))
146
+ logger.trace(paths)
147
+ assert len(paths) == 1
148
+ return paths[0]
149
+
150
+ logger.trace(G.edge_subgraph(sg.edges).edges)
151
+ assert sg.order() == 2
152
+ assert len(sg.edges) == 1
153
+ e = list(sg.edges)[0]
154
+ return [e[0], e[1]]
155
+ # TODO: some stronger conditions on this.. this is assuming that everuthing is connected..
156
+ return list(sg.edges)
157
+
158
+
159
+ def find_small_node_surfaces(G: nx.DiGraph, nodes: Iterable[str]) -> list[Surface]:
160
+ def find(node: str):
161
+ res = G.nodes[node].get("data")
162
+ assert isinstance(res, NodeData)
163
+ s = res.surface
164
+ assert s
165
+ return s
166
+
167
+ return [find(n) for n in nodes]
168
+
169
+
170
+ def handle_components(G: nx.DiGraph, nodes_: Iterable[str]):
171
+ nodes = list(nodes_)
172
+ if len(nodes) >= 2:
173
+ ordered_nodes = order_nodes_based_on_graph(G, nodes)
174
+ logger.trace(pretty_repr({"orig_nodes": nodes, "ordered": ordered_nodes}))
175
+ else:
176
+ ordered_nodes = nodes
177
+ return find_small_node_surfaces(G, ordered_nodes)
178
+
179
+
180
+ def get_surface(G: nx.Graph, node: str):
181
+ data = G.nodes[node].get("data")
182
+ assert isinstance(data, NodeData)
183
+ s = data.surface
184
+ assert s
185
+ return s
186
+
187
+
188
+ def repr_graph(G: nx.DiGraph):
189
+ fd = {}
190
+ for node, d in G.nodes(data=True):
191
+ # TODO: can clean up with get method..
192
+ data = d["data"]
193
+ assert isinstance(data, NodeData)
194
+ fd[node] = data.repr_dict
195
+ return pretty_repr(fd, expand_all=True)