brushline 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. brushline/__init__.py +7 -0
  2. brushline/build/__init__.py +7 -0
  3. brushline/build/compiler.py +278 -0
  4. brushline/cli.py +476 -0
  5. brushline/demo/__init__.py +1 -0
  6. brushline/demo/builder.py +103 -0
  7. brushline/demo/deploy.py +32 -0
  8. brushline/demo/pilot.py +80 -0
  9. brushline/demo/portfolio.py +103 -0
  10. brushline/demo/validation.py +48 -0
  11. brushline/exchange/__init__.py +31 -0
  12. brushline/exchange/audit.py +63 -0
  13. brushline/exchange/bids.py +67 -0
  14. brushline/exchange/contractor.py +60 -0
  15. brushline/exchange/owner.py +51 -0
  16. brushline/exchange/plan.py +102 -0
  17. brushline/exchange/validation.py +131 -0
  18. brushline/fixtures/__init__.py +19 -0
  19. brushline/fixtures/synthetic.py +1042 -0
  20. brushline/instance_ops.py +305 -0
  21. brushline/instance_template.py +81 -0
  22. brushline/integrations/__init__.py +23 -0
  23. brushline/integrations/canopy.py +700 -0
  24. brushline/integrations/capabilities.py +950 -0
  25. brushline/integrations/catalogue.py +805 -0
  26. brushline/integrations/femic.py +318 -0
  27. brushline/integrations/fetch.py +1112 -0
  28. brushline/integrations/freshforge.py +447 -0
  29. brushline/integrations/lidar.py +800 -0
  30. brushline/integrations/lidar_benchmark.py +466 -0
  31. brushline/integrations/lidar_coverage.py +533 -0
  32. brushline/integrations/terrain.py +586 -0
  33. brushline/model/__init__.py +71 -0
  34. brushline/model/archive.py +264 -0
  35. brushline/model/corridor.py +559 -0
  36. brushline/model/demo.py +217 -0
  37. brushline/model/plan.py +337 -0
  38. brushline/model/run.py +164 -0
  39. brushline/model/schema.py +587 -0
  40. brushline/model/state.py +530 -0
  41. brushline/model/timeline.py +167 -0
  42. brushline/model/validation.py +101 -0
  43. brushline/monitoring/__init__.py +69 -0
  44. brushline/monitoring/mission.py +282 -0
  45. brushline/monitoring/observation.py +165 -0
  46. brushline/monitoring/recalibration.py +107 -0
  47. brushline/monitoring/replan.py +58 -0
  48. brushline/monitoring/validation.py +102 -0
  49. brushline/monitoring/value.py +225 -0
  50. brushline/optimization/__init__.py +199 -0
  51. brushline/optimization/adapter.py +877 -0
  52. brushline/optimization/budget.py +551 -0
  53. brushline/optimization/evaluator.py +966 -0
  54. brushline/optimization/formulation.py +681 -0
  55. brushline/optimization/operators.py +910 -0
  56. brushline/optimization/pareto.py +511 -0
  57. brushline/optimization/telemetry.py +673 -0
  58. brushline/optimization/validation.py +349 -0
  59. brushline/resources/__init__.py +3 -0
  60. brushline/resources/instance/QUICKSTART.md +28 -0
  61. brushline/resources/instance/README.md +34 -0
  62. brushline/resources/instance/__init__.py +3 -0
  63. brushline/resources/instance/config/brushline/rebuild.steps.yaml +57 -0
  64. brushline/resources/instance/config/brushline/scenario.yaml +11 -0
  65. brushline/resources/instance/config/brushline/standards.yaml +5 -0
  66. brushline/resources/instance/config/brushline/targets.yaml +5 -0
  67. brushline/resources/instance/config/rebuild.allowlist.yaml +8 -0
  68. brushline/resources/instance/config/rebuild.spec.yaml +78 -0
  69. brushline/resources/instance/evidence/README.md +11 -0
  70. brushline/resources/instance/runbooks/REBUILD_RUNBOOK.md +54 -0
  71. brushline/resources/instance/tests/README.md +9 -0
  72. brushline/run.py +128 -0
  73. brushline/sim/__init__.py +14 -0
  74. brushline/sim/engine.py +363 -0
  75. brushline/sim/schedule.py +124 -0
  76. brushline/state_init.py +859 -0
  77. brushline/strategic/__init__.py +159 -0
  78. brushline/strategic/bau.py +453 -0
  79. brushline/strategic/dynamic_program.py +492 -0
  80. brushline/strategic/monitoring.py +417 -0
  81. brushline/strategic/portfolio.py +787 -0
  82. brushline/strategic/reports.py +294 -0
  83. brushline/strategic/scenario.py +462 -0
  84. brushline/strategic/validation.py +450 -0
  85. brushline/tactical/__init__.py +132 -0
  86. brushline/tactical/access.py +219 -0
  87. brushline/tactical/fhops.py +251 -0
  88. brushline/tactical/operations.py +252 -0
  89. brushline/tactical/program.py +323 -0
  90. brushline/tactical/resources.py +283 -0
  91. brushline/tactical/timeline.py +220 -0
  92. brushline/tactical/validation.py +389 -0
  93. brushline/vegetation/__init__.py +129 -0
  94. brushline/vegetation/calibration.py +710 -0
  95. brushline/vegetation/growth.py +592 -0
  96. brushline/vegetation/hazards.py +644 -0
  97. brushline/vegetation/model_card.py +475 -0
  98. brushline/vegetation/state_space.py +703 -0
  99. brushline/vegetation/treatment.py +589 -0
  100. brushline/vegetation/validation.py +704 -0
  101. brushline/verification/__init__.py +1 -0
  102. brushline/verification/acceptance.py +251 -0
  103. brushline/web/__init__.py +4 -0
  104. brushline/web/model_builder.py +1362 -0
  105. brushline/web/static/model-builder.css +176 -0
  106. brushline/web/static/model-builder.html +142 -0
  107. brushline/web/static/model-builder.js +509 -0
  108. brushline/web/static/studio.css +1 -0
  109. brushline/web/static/studio.html +13 -0
  110. brushline/web/static/studio.js +41 -0
  111. brushline/web/studio.py +233 -0
  112. brushline-0.1.0.dist-info/METADATA +162 -0
  113. brushline-0.1.0.dist-info/RECORD +117 -0
  114. brushline-0.1.0.dist-info/WHEEL +5 -0
  115. brushline-0.1.0.dist-info/entry_points.txt +5 -0
  116. brushline-0.1.0.dist-info/licenses/LICENSE +201 -0
  117. brushline-0.1.0.dist-info/top_level.txt +1 -0
brushline/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Brushline: decision support for vegetation management along linear corridors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,7 @@
1
+ """Build and compile paths for Brushline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from brushline.build.compiler import CompiledDataset, compile_fixture
6
+
7
+ __all__ = ["CompiledDataset", "compile_fixture"]
@@ -0,0 +1,278 @@
1
+ """Compile the synthetic corridor fixture into a validated Brushline dataset.
2
+
3
+ The compiler reads the fixture's dataset, corridor graph, and state contract, joins the service
4
+ standards and corridor attributes onto the segments, and writes the compiled segments and states
5
+ tables plus a dataset manifest. It is the smallest end-to-end compile path; the production compiler
6
+ is P2 and P3.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ import pandas as pd
16
+
17
+ from brushline.model.corridor import CorridorNetwork, validate_corridor_network
18
+ from brushline.model.schema import (
19
+ ColumnSpec,
20
+ DatasetManifest,
21
+ Provenance,
22
+ TableSpec,
23
+ file_sha256,
24
+ load_manifest,
25
+ validate_dataset,
26
+ write_manifest,
27
+ )
28
+ from brushline.model.state import StateContract, validate_state_contract
29
+
30
+ SEGMENT_COLUMNS: tuple[str, ...] = (
31
+ "segment_id",
32
+ "corridor_id",
33
+ "length_m",
34
+ "geometry_wkt",
35
+ "span_id",
36
+ "sequence",
37
+ "treatment_unit_id",
38
+ "service_standard_id",
39
+ "access_point_id",
40
+ "max_height_m",
41
+ "min_clearance_m",
42
+ )
43
+ STATE_COLUMNS: tuple[str, ...] = (
44
+ "segment_id",
45
+ "state_year",
46
+ "functional_group",
47
+ "state_class",
48
+ "height_m",
49
+ "cover_pct",
50
+ "density_per_ha",
51
+ "age_years",
52
+ "uncertainty_confidence",
53
+ "source",
54
+ "source_version",
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class CompiledDataset:
60
+ """The output of one fixture compile.
61
+
62
+ Parameters
63
+ ----------
64
+ root : Path
65
+ Compiled dataset directory.
66
+ manifest : DatasetManifest
67
+ Dataset manifest.
68
+ segments : pandas.DataFrame
69
+ Compiled segment table.
70
+ states : pandas.DataFrame
71
+ Compiled state table.
72
+ """
73
+
74
+ root: Path
75
+ manifest: DatasetManifest
76
+ segments: pd.DataFrame
77
+ states: pd.DataFrame
78
+
79
+
80
+ def compile_fixture(
81
+ fixture_root: Path,
82
+ out: Path,
83
+ *,
84
+ force: bool = False,
85
+ ) -> CompiledDataset:
86
+ """Compile the synthetic fixture into a validated dataset.
87
+
88
+ Parameters
89
+ ----------
90
+ fixture_root : Path
91
+ Fixture directory, such as ``examples/synthetic-corridor``.
92
+ out : Path
93
+ Compiled dataset directory.
94
+ force : bool, optional
95
+ Overwrite an existing dataset directory. Defaults to ``False``.
96
+
97
+ Returns
98
+ -------
99
+ CompiledDataset
100
+ The compiled directory, manifest, segments, and states.
101
+
102
+ Raises
103
+ ------
104
+ FileExistsError
105
+ When the output directory is not empty and ``force`` is false.
106
+ ValueError
107
+ When a source contract fails validation.
108
+ """
109
+ fixture = Path(fixture_root).expanduser().resolve()
110
+ source_report = validate_dataset(fixture / "dataset")
111
+ if not source_report.is_valid:
112
+ raise ValueError("; ".join(source_report.errors))
113
+ source_manifest = load_manifest(fixture / "dataset")
114
+ segments_source = pd.read_csv(
115
+ fixture / "dataset" / "segments.csv",
116
+ dtype={
117
+ "segment_id": "string",
118
+ "corridor_id": "string",
119
+ "geometry_wkt": "string",
120
+ },
121
+ )
122
+ network = CorridorNetwork.model_validate_json((fixture / "corridor.json").read_text())
123
+ network_report = validate_corridor_network(network)
124
+ if not network_report.is_valid:
125
+ raise ValueError("; ".join(network_report.errors))
126
+ states_source = StateContract.model_validate_json((fixture / "state.json").read_text())
127
+ state_report = validate_state_contract(
128
+ states_source,
129
+ required_segment_ids=set(segments_source["segment_id"].astype(str)),
130
+ )
131
+ if not state_report.is_valid:
132
+ raise ValueError("; ".join(state_report.errors))
133
+
134
+ standards = pd.DataFrame(
135
+ [
136
+ {
137
+ "service_standard_id": standard.service_standard_id,
138
+ "max_height_m": standard.max_height_m,
139
+ "min_clearance_m": standard.min_clearance_m,
140
+ }
141
+ for standard in network.service_standards
142
+ ]
143
+ )
144
+ corridor_rows = pd.DataFrame(
145
+ [
146
+ {
147
+ "segment_id": segment.segment_id,
148
+ "span_id": segment.span_id,
149
+ "sequence": segment.sequence,
150
+ "treatment_unit_id": segment.treatment_unit_id,
151
+ "service_standard_id": segment.service_standard_id,
152
+ "access_point_id": segment.access_point_id,
153
+ }
154
+ for segment in network.segments
155
+ ]
156
+ )
157
+ segments = segments_source.merge(
158
+ corridor_rows,
159
+ on="segment_id",
160
+ how="left",
161
+ validate="one_to_one",
162
+ ).merge(standards, on="service_standard_id", how="left", validate="many_to_one")
163
+ if segments[["span_id", "max_height_m", "min_clearance_m"]].isna().any().any():
164
+ raise ValueError("a segment is missing a span or a service standard")
165
+ segments["access_point_id"] = segments["access_point_id"].astype("string")
166
+ segments["sequence"] = segments["sequence"].astype("int64")
167
+ segments = segments[list(SEGMENT_COLUMNS)]
168
+
169
+ states = pd.DataFrame(
170
+ [
171
+ {
172
+ "segment_id": state.segment_id,
173
+ "state_year": state.state_year,
174
+ "functional_group": state.functional_group,
175
+ "state_class": state.state_class,
176
+ "height_m": state.height_m,
177
+ "cover_pct": state.cover_pct,
178
+ "density_per_ha": state.density_per_ha,
179
+ "age_years": state.age_years,
180
+ "uncertainty_confidence": state.uncertainty.confidence,
181
+ "source": state.source,
182
+ "source_version": state.source_version,
183
+ }
184
+ for state in states_source.states
185
+ ]
186
+ )
187
+ states["density_per_ha"] = states["density_per_ha"].astype("float64")
188
+ states["age_years"] = states["age_years"].astype("float64")
189
+ states = states[list(STATE_COLUMNS)]
190
+
191
+ target = Path(out).expanduser().resolve()
192
+ if target.exists() and any(target.iterdir()) and not force:
193
+ raise FileExistsError(f"{target} is not empty; pass force=True to overwrite")
194
+ target.mkdir(parents=True, exist_ok=True)
195
+ segments_path = target / "segments.parquet"
196
+ states_path = target / "states.parquet"
197
+ segments.to_parquet(segments_path, index=False)
198
+ states.to_parquet(states_path, index=False)
199
+
200
+ manifest = DatasetManifest(
201
+ dataset_id=f"{source_manifest.dataset_id}-compiled",
202
+ name="Synthetic corridor compiled dataset",
203
+ description="The P1.6 prototype compile of the synthetic corridor fixture.",
204
+ created_utc=datetime.now(UTC),
205
+ crs=source_manifest.crs,
206
+ units=source_manifest.units,
207
+ tables=[
208
+ TableSpec(
209
+ name="segments",
210
+ path="segments.parquet",
211
+ format="parquet",
212
+ primary_key=["segment_id"],
213
+ columns=_segment_columns(),
214
+ row_count=len(segments),
215
+ sha256=file_sha256(segments_path),
216
+ ),
217
+ TableSpec(
218
+ name="states",
219
+ path="states.parquet",
220
+ format="parquet",
221
+ primary_key=["segment_id", "state_year", "functional_group"],
222
+ columns=_state_columns(),
223
+ row_count=len(states),
224
+ sha256=file_sha256(states_path),
225
+ ),
226
+ ],
227
+ provenance=Provenance(
228
+ source="synthetic-corridor",
229
+ source_version="1.0",
230
+ licence="CC0-1.0",
231
+ created_by="brushline.build.compiler",
232
+ configuration={"compiler": "p1.6-prototype"},
233
+ ),
234
+ content_digest="0" * 64,
235
+ )
236
+ manifest.content_digest = manifest.compute_content_digest()
237
+ write_manifest(target, manifest)
238
+ return CompiledDataset(target, manifest, segments, states)
239
+
240
+
241
+ def _segment_columns() -> list[ColumnSpec]:
242
+ return [
243
+ ColumnSpec(name="segment_id", dtype="string"),
244
+ ColumnSpec(name="corridor_id", dtype="string"),
245
+ ColumnSpec(name="length_m", dtype="float64", unit="m"),
246
+ ColumnSpec(name="geometry_wkt", dtype="string"),
247
+ ColumnSpec(name="span_id", dtype="string"),
248
+ ColumnSpec(name="sequence", dtype="int64"),
249
+ ColumnSpec(name="treatment_unit_id", dtype="string"),
250
+ ColumnSpec(name="service_standard_id", dtype="string"),
251
+ ColumnSpec(name="access_point_id", dtype="string", required=False),
252
+ ColumnSpec(name="max_height_m", dtype="float64", unit="m"),
253
+ ColumnSpec(name="min_clearance_m", dtype="float64", unit="m"),
254
+ ]
255
+
256
+
257
+ def _state_columns() -> list[ColumnSpec]:
258
+ return [
259
+ ColumnSpec(name="segment_id", dtype="string"),
260
+ ColumnSpec(name="state_year", dtype="int64"),
261
+ ColumnSpec(name="functional_group", dtype="string"),
262
+ ColumnSpec(name="state_class", dtype="string"),
263
+ ColumnSpec(name="height_m", dtype="float64", unit="m"),
264
+ ColumnSpec(name="cover_pct", dtype="float64", unit="%"),
265
+ ColumnSpec(name="density_per_ha", dtype="float64", unit="stems/ha", required=False),
266
+ ColumnSpec(name="age_years", dtype="float64", unit="years", required=False),
267
+ ColumnSpec(name="uncertainty_confidence", dtype="float64"),
268
+ ColumnSpec(name="source", dtype="string"),
269
+ ColumnSpec(name="source_version", dtype="string"),
270
+ ]
271
+
272
+
273
+ __all__ = [
274
+ "SEGMENT_COLUMNS",
275
+ "STATE_COLUMNS",
276
+ "CompiledDataset",
277
+ "compile_fixture",
278
+ ]