beefcot 1.0.3__tar.gz → 1.0.4__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.
- {beefcot-1.0.3 → beefcot-1.0.4}/.gitignore +3 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/PKG-INFO +13 -1
- {beefcot-1.0.3 → beefcot-1.0.4}/README.pypi.md +8 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/pyproject.toml +4 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/__init__.py +2 -2
- beefcot-1.0.4/src/beefcot/__main__.py +5 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cli.py +137 -3
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cli_presentation.py +30 -1
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cutting_plan/economics.py +68 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cutting_plan/run.py +13 -1
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/learners.py +10 -1
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/transforms.py +14 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/blocks.py +12 -2
- beefcot-1.0.4/src/beefcot/project.py +202 -0
- beefcot-1.0.4/src/beefcot/project_registry.py +150 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/registry.py +1 -1
- beefcot-1.0.4/src/beefcot/regression/assembly.py +397 -0
- beefcot-1.0.4/src/beefcot/regression/assembly_network.py +61 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/exhaustive.py +146 -37
- beefcot-1.0.4/src/beefcot/regression/exhaustive_parallel.py +110 -0
- beefcot-1.0.4/src/beefcot/regression/exhaustive_storage.py +182 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/run.py +6 -1
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/spec.py +35 -6
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/run.py +46 -5
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/schemas/model-3.schema.json +3 -1
- beefcot-1.0.4/src/beefcot/service/__init__.py +5 -0
- beefcot-1.0.4/src/beefcot/service/app.py +512 -0
- beefcot-1.0.4/src/beefcot/service/auth.py +219 -0
- beefcot-1.0.4/src/beefcot/service/errors.py +11 -0
- beefcot-1.0.4/src/beefcot/service/plans.py +129 -0
- beefcot-1.0.4/src/beefcot/service/projects.py +63 -0
- beefcot-1.0.4/src/beefcot/service/runner.py +109 -0
- beefcot-1.0.4/src/beefcot/service/server.py +23 -0
- beefcot-1.0.4/src/beefcot/service/studies.py +206 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/anomaly.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/chain.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/anomaly.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/anomaly_figures.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/bins.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/cut_tree.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/figures.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/forest.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/report.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/review.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/run.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/spec.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/checks/table.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cutting_plan/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/cutting_plan/solver.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/analysis.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/fits.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/report.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/run.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/sources.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/decomposition/spec.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/analysis.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/methods.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/plots.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/report.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/run.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/sources.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/diagnostics/spec.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/expression.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/functional.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/learner_registry.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/rows.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/runtime.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/topology.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model/types.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/card.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/documents.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/export.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/fitted.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/library.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/model_format/schema.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/prediction/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/prediction/run.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/producers/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/producers/network.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/producers/rf/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/producers/rf/loop.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/producers/rf/relevance.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/anomaly.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/candidates.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/factory.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/mapping_review.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/mapping_space.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/provenance.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_checkpoint.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_controller.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_effort.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_equivalence.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_policy.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_selection.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_settings.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rerf_warm_start.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/residual.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/rf_expression.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/regression/table.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/metrics.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/plots.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/regression_space.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/reports/style.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/resources/cut_tree.default.json +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/resources/rerf_space.default.yaml +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/__init__.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/expression_ast.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/loader.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/model.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/schemas/checks-3.schema.json +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/schemas/predictors-3.schema.json +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/schemas/request-3.schema.json +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/runtime/schemas/result-3.schema.json +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/workspace.py +0 -0
- {beefcot-1.0.3 → beefcot-1.0.4}/src/beefcot/yamlio.py +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: beefcot
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Carcass yield models and cutting plans: one YAML in, one delivered model out
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: fastapi>=0.115
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
6
8
|
Requires-Dist: jarvisplot>=2.1
|
|
7
9
|
Requires-Dist: numpy>=2.5
|
|
8
10
|
Requires-Dist: pandas>=3.0
|
|
@@ -10,6 +12,8 @@ Requires-Dist: pytest>=8
|
|
|
10
12
|
Requires-Dist: pyyaml>=6.0
|
|
11
13
|
Requires-Dist: scikit-learn>=1.9
|
|
12
14
|
Requires-Dist: scipy>=1.18
|
|
15
|
+
Requires-Dist: threadpoolctl>=3.6
|
|
16
|
+
Requires-Dist: uvicorn>=0.32
|
|
13
17
|
Requires-Dist: xgboost>=3.3
|
|
14
18
|
Description-Content-Type: text/markdown
|
|
15
19
|
|
|
@@ -23,8 +27,16 @@ python3 -m pip install beefcot
|
|
|
23
27
|
beefcot --help
|
|
24
28
|
beefcot validate /path/to/job.yaml
|
|
25
29
|
beefcot fit /path/to/job.yaml
|
|
30
|
+
beefcot project create HERD
|
|
31
|
+
beefcot project list
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
`beefcot project create` writes one project directory. The machine list
|
|
35
|
+
defaults to `~/.beefcot/projects.json`. Pass `--projects` or set
|
|
36
|
+
`BEEFCOT_PROJECTS` to use another file. `beefcot serve` reads that list and
|
|
37
|
+
opens a loopback workbench. The HTML pages stay with the source checkout and
|
|
38
|
+
are not part of this package.
|
|
39
|
+
|
|
28
40
|
Each fit starts from a `kind: regression_tree` YAML declaring its input table,
|
|
29
41
|
feature and target columns, cut-tree mapping, split and regression space.
|
|
30
42
|
Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
|
|
@@ -8,8 +8,16 @@ python3 -m pip install beefcot
|
|
|
8
8
|
beefcot --help
|
|
9
9
|
beefcot validate /path/to/job.yaml
|
|
10
10
|
beefcot fit /path/to/job.yaml
|
|
11
|
+
beefcot project create HERD
|
|
12
|
+
beefcot project list
|
|
11
13
|
```
|
|
12
14
|
|
|
15
|
+
`beefcot project create` writes one project directory. The machine list
|
|
16
|
+
defaults to `~/.beefcot/projects.json`. Pass `--projects` or set
|
|
17
|
+
`BEEFCOT_PROJECTS` to use another file. `beefcot serve` reads that list and
|
|
18
|
+
opens a loopback workbench. The HTML pages stay with the source checkout and
|
|
19
|
+
are not part of this package.
|
|
20
|
+
|
|
13
21
|
Each fit starts from a `kind: regression_tree` YAML declaring its input table,
|
|
14
22
|
feature and target columns, cut-tree mapping, split and regression space.
|
|
15
23
|
Available methods are profile, RF, DNN, ReRF and exhaustive search. Structural
|
|
@@ -16,10 +16,14 @@ dependencies = [
|
|
|
16
16
|
"pandas>=3.0",
|
|
17
17
|
"scipy>=1.18",
|
|
18
18
|
"scikit-learn>=1.9",
|
|
19
|
+
"threadpoolctl>=3.6",
|
|
19
20
|
"xgboost>=3.3",
|
|
20
21
|
"PyYAML>=6.0",
|
|
21
22
|
"JarvisPLOT>=2.1",
|
|
22
23
|
"pytest>=8",
|
|
24
|
+
"fastapi>=0.115",
|
|
25
|
+
"uvicorn>=0.32",
|
|
26
|
+
"httpx>=0.27",
|
|
23
27
|
]
|
|
24
28
|
|
|
25
29
|
[project.scripts]
|
|
@@ -6,7 +6,7 @@ Every job is one YAML and one verb, the same in Python as on the command line (`
|
|
|
6
6
|
|
|
7
7
|
beefcot.fit("config/regression/murdoch_rf.yaml") # kind: regression_tree -> a run with a model
|
|
8
8
|
beefcot.predict("config/prediction/murdoch_rf_241.yaml") # kind: prediction -> one table
|
|
9
|
-
beefcot.plan("config/plan/
|
|
9
|
+
beefcot.plan("config/plan/breakdown.yaml") # kind: cutting_plan -> a plan
|
|
10
10
|
beefcot.check("config/checks/murdoch_core3_bins.yaml") # kind: bin_variance -> a data check
|
|
11
11
|
beefcot.decompose("config/decomposition/murdoch_rf.yaml") # kind: error_decomposition -> noise / bias² / variance
|
|
12
12
|
beefcot.run("config/pipelines/murdoch_smoke.yaml") # kind: pipeline -> several, in order
|
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
27
|
-
__version__ = "1.0.
|
|
27
|
+
__version__ = "1.0.4" # the one place it is written; CHANGELOG.md records each version
|
|
28
28
|
|
|
29
29
|
__all__ = ["__version__", "fit", "predict", "plan", "check", "decompose", "run", "load_model"]
|
|
30
30
|
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
beefcot fit config/regression/murdoch_rf.yaml # kind: regression_tree -> a run with a model
|
|
4
4
|
beefcot predict config/prediction/murdoch_rf_241.yaml # kind: prediction -> one table
|
|
5
|
-
beefcot plan config/plan/
|
|
5
|
+
beefcot plan config/plan/breakdown.yaml # kind: cutting_plan -> a plan
|
|
6
6
|
beefcot check config/checks/murdoch_core3_bins.yaml # kind: bin_variance -> a data check
|
|
7
7
|
beefcot decompose config/decomposition/murdoch_rf.yaml # kind: error_decomposition -> noise / bias² / variance
|
|
8
8
|
beefcot run config/pipelines/murdoch_smoke.yaml # kind: pipeline -> several, in order
|
|
9
9
|
beefcot validate <any YAML> # read and check it, compute nothing
|
|
10
10
|
beefcot models list | show ID | verify ID | import PATH | export ID OUT
|
|
11
|
+
beefcot serve # loopback workbench; --design draws the column grid
|
|
12
|
+
beefcot user add NAME # create a workbench login
|
|
13
|
+
beefcot project create PATH # a standard project and an empty model library
|
|
14
|
+
beefcot project validate [PATH] # check that project; compute nothing
|
|
15
|
+
beefcot project list # projects recorded on this machine
|
|
16
|
+
beefcot project add PATH # record an existing project
|
|
17
|
+
beefcot project remove ID # drop it from the list; keep the directory
|
|
11
18
|
beefcot kinds | version
|
|
12
19
|
beefcot -V | -v | --version # package version, no job required
|
|
13
20
|
|
|
@@ -35,6 +42,33 @@ PRODUCING = ("fit", "predict", "plan", "check", "decompose", "run")
|
|
|
35
42
|
WITH_LIBRARY = ("predict", "plan", "run") # the jobs that may look a model up by id
|
|
36
43
|
|
|
37
44
|
|
|
45
|
+
def _user(args: argparse.Namespace) -> int:
|
|
46
|
+
"""Create or list workbench accounts. The password is read from the terminal."""
|
|
47
|
+
import getpass
|
|
48
|
+
|
|
49
|
+
from beefcot.service.auth import Accounts, AuthError
|
|
50
|
+
|
|
51
|
+
root = Path(args.root).expanduser() if args.root else Path.cwd()
|
|
52
|
+
accounts = Accounts(root / "config" / "auth" / "users.yaml")
|
|
53
|
+
try:
|
|
54
|
+
if args.user_command == "list":
|
|
55
|
+
for name in accounts.names():
|
|
56
|
+
print(name)
|
|
57
|
+
return 0
|
|
58
|
+
if not sys.stdin.isatty():
|
|
59
|
+
raise AuthError(422, "INVALID_PASSWORD", "run beefcot user add in a terminal so the password is not saved in a command")
|
|
60
|
+
password = getpass.getpass("Password: ")
|
|
61
|
+
repeated = getpass.getpass("Repeat password: ")
|
|
62
|
+
if password != repeated:
|
|
63
|
+
raise AuthError(422, "INVALID_PASSWORD", "the two passwords did not match")
|
|
64
|
+
accounts.add(args.name, password)
|
|
65
|
+
except AuthError as failure:
|
|
66
|
+
print(failure.message, file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
print(f"added {args.name}")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
38
72
|
def _produce(args: argparse.Namespace) -> dict[str, Any]:
|
|
39
73
|
import beefcot
|
|
40
74
|
from beefcot import registry
|
|
@@ -135,6 +169,26 @@ def _models(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
135
169
|
return {"ok": True, "id": args.id, "written": str(written)}
|
|
136
170
|
|
|
137
171
|
|
|
172
|
+
def _project(args: argparse.Namespace) -> dict[str, Any]:
|
|
173
|
+
"""Create or check a project, or change the machine list. None of these fit a model."""
|
|
174
|
+
from beefcot.project import create, validate
|
|
175
|
+
from beefcot.project_registry import add, describe, location, remove
|
|
176
|
+
|
|
177
|
+
registry = location(getattr(args, "projects", None))
|
|
178
|
+
if args.project_command == "create":
|
|
179
|
+
report = create(args.path)
|
|
180
|
+
report.update(add(registry, report["project"]))
|
|
181
|
+
report["registry"] = str(registry)
|
|
182
|
+
return report
|
|
183
|
+
if args.project_command == "add":
|
|
184
|
+
return {"ok": True, "registry": str(registry), **add(registry, args.path)}
|
|
185
|
+
if args.project_command == "remove":
|
|
186
|
+
return {"ok": True, "registry": str(registry), **remove(registry, args.id)}
|
|
187
|
+
if args.project_command == "list":
|
|
188
|
+
return {"ok": True, "registry": str(registry), "projects": describe(registry)}
|
|
189
|
+
return validate(args.path or Path.cwd())
|
|
190
|
+
|
|
191
|
+
|
|
138
192
|
def _kinds() -> dict[str, Any]:
|
|
139
193
|
from beefcot import registry
|
|
140
194
|
|
|
@@ -146,9 +200,10 @@ def _parser(presentation: Terminal | None = None) -> argparse.ArgumentParser:
|
|
|
146
200
|
parser = HelpParser(prog="beefcot", presentation=presentation,
|
|
147
201
|
description="Define a job in YAML. Validate it, run it, inspect the delivered results.",
|
|
148
202
|
help_panels=[('Regression & data', ('validate', 'fit', 'predict', 'check', 'decompose')),
|
|
149
|
-
('Planning & pipelines', ('plan', 'run')),
|
|
203
|
+
('Planning & pipelines', ('plan', 'run', 'serve', 'user', 'project')),
|
|
150
204
|
('Models & reference', ('models', 'kinds', 'version'))],
|
|
151
205
|
examples=[('Validate', 'beefcot validate path/to/job.yaml'),
|
|
206
|
+
('New project', 'beefcot project create HERD'),
|
|
152
207
|
('Fit', 'beefcot fit config/regression/murdoch_core3_rf.yaml'),
|
|
153
208
|
('Command help', 'beefcot fit --help')])
|
|
154
209
|
parser.add_argument('-V', '-v', '--version', action=VersionAction,
|
|
@@ -186,6 +241,63 @@ def _parser(presentation: Terminal | None = None) -> argparse.ArgumentParser:
|
|
|
186
241
|
command(name, helps[name], library=name in WITH_LIBRARY, quiet=True).add_argument("config", metavar="YAML", help="job configuration; paths inside it are relative to this YAML")
|
|
187
242
|
command("validate", "validate a YAML and its paths without running the job", library=True).add_argument(
|
|
188
243
|
"config", metavar="YAML", help="the configuration to validate")
|
|
244
|
+
serve = sub.add_parser("serve", help="serve the local cutting-plan workbench on 127.0.0.1",
|
|
245
|
+
description="Serve the local cutting-plan workbench. The host is loopback only.",
|
|
246
|
+
examples=[("Open", "beefcot serve"), ("Grid", "beefcot serve --design")])
|
|
247
|
+
presentation_options(serve)
|
|
248
|
+
serve.add_argument("--port", type=int, default=8765, help="loopback port (default: 8765)")
|
|
249
|
+
serve.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
|
|
250
|
+
serve.add_argument("--design", action="store_true", help="draw the 12-column reference grid in magenta")
|
|
251
|
+
serve.add_argument("--projects", default=None, help="the machine project list (default: ~/.beefcot/projects.json)")
|
|
252
|
+
|
|
253
|
+
account = sub.add_parser("user", help="add or list workbench logins",
|
|
254
|
+
description="Add a workbench login. The password is typed, not passed as an argument. "
|
|
255
|
+
"The web page cannot register a user.",
|
|
256
|
+
examples=[("Add", "beefcot user add NAME"), ("List", "beefcot user list")])
|
|
257
|
+
presentation_options(account)
|
|
258
|
+
account_commands = account.add_subparsers(dest="user_command", required=True)
|
|
259
|
+
added = account_commands.add_parser("add", help="create one user in config/auth/users.yaml")
|
|
260
|
+
presentation_options(added)
|
|
261
|
+
added.add_argument("name", help="the name typed on the login page")
|
|
262
|
+
added.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
|
|
263
|
+
listed = account_commands.add_parser("list", help="list user names")
|
|
264
|
+
presentation_options(listed)
|
|
265
|
+
listed.add_argument("--root", default=None, help="project directory that contains config/ (default: the current directory)")
|
|
266
|
+
|
|
267
|
+
project = sub.add_parser(
|
|
268
|
+
"project",
|
|
269
|
+
help="create or validate a project directory",
|
|
270
|
+
description="A project holds tables, job YAML and a model library. The package stays separate.",
|
|
271
|
+
examples=[("Create", "beefcot project create HERD"),
|
|
272
|
+
("Validate", "beefcot project validate HERD"),
|
|
273
|
+
("Register", "beefcot project add HERD"),
|
|
274
|
+
("On this machine", "beefcot project list")],
|
|
275
|
+
)
|
|
276
|
+
presentation_options(project)
|
|
277
|
+
project_commands = project.add_subparsers(dest="project_command", required=True)
|
|
278
|
+
|
|
279
|
+
def project_command(name: str, help_text: str, description: str) -> argparse.ArgumentParser:
|
|
280
|
+
made = project_commands.add_parser(name, help=help_text, description=description)
|
|
281
|
+
presentation_options(made)
|
|
282
|
+
made.add_argument("--projects", default=None, help="the machine project list (default: ~/.beefcot/projects.json)")
|
|
283
|
+
return made
|
|
284
|
+
|
|
285
|
+
created = project_command("create", "write a standard project and an empty model library",
|
|
286
|
+
"Write a standard project and an empty model library. "
|
|
287
|
+
"The directory must be missing or empty. No table is copied. "
|
|
288
|
+
"The new project is recorded in the machine list.")
|
|
289
|
+
created.add_argument("path", help="the new project directory")
|
|
290
|
+
checked = project_command("validate", "check a project directory and compute nothing",
|
|
291
|
+
"Check a project directory and compute nothing.")
|
|
292
|
+
checked.add_argument("path", nargs="?", default=None, help="the project directory (default: the current directory)")
|
|
293
|
+
project_command("list", "list the projects recorded on this machine",
|
|
294
|
+
"List the projects recorded on this machine.")
|
|
295
|
+
added = project_command("add", "record an existing project directory",
|
|
296
|
+
"Record an existing project directory. The directory is not copied.")
|
|
297
|
+
added.add_argument("path", help="the project directory")
|
|
298
|
+
removed = project_command("remove", "drop a project from the machine list",
|
|
299
|
+
"Drop a project from the machine list. The directory is kept.")
|
|
300
|
+
removed.add_argument("id", help="the project id")
|
|
189
301
|
|
|
190
302
|
models = sub.add_parser("models", help="inspect, verify and move delivered models",
|
|
191
303
|
description="Inspect, verify and move delivered models.",
|
|
@@ -224,6 +336,21 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
224
336
|
parser.print_help()
|
|
225
337
|
return 0
|
|
226
338
|
args = parser.parse_args(raw)
|
|
339
|
+
if args.command == "serve":
|
|
340
|
+
from beefcot.service.errors import ServiceError
|
|
341
|
+
from beefcot.service.server import serve
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
serve(Path(args.root).expanduser() if args.root else Path.cwd(), args.port,
|
|
345
|
+
design=args.design, projects=args.projects)
|
|
346
|
+
except ServiceError as failure:
|
|
347
|
+
print(failure.message, file=sys.stderr)
|
|
348
|
+
return 1
|
|
349
|
+
except KeyboardInterrupt:
|
|
350
|
+
return 130
|
|
351
|
+
return 0
|
|
352
|
+
if args.command == "user":
|
|
353
|
+
return _user(args)
|
|
227
354
|
ui.as_json = getattr(args, 'json', False)
|
|
228
355
|
ui.no_color = getattr(args, 'no_color', False)
|
|
229
356
|
quiet = getattr(args, 'quiet', False)
|
|
@@ -241,6 +368,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
241
368
|
result = _produce(args)
|
|
242
369
|
elif args.command == "validate":
|
|
243
370
|
result = _validate(args)
|
|
371
|
+
elif args.command == "project":
|
|
372
|
+
result = _project(args)
|
|
244
373
|
elif args.command == "models":
|
|
245
374
|
result = _models(args)
|
|
246
375
|
elif args.command == "kinds":
|
|
@@ -254,7 +383,12 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
254
383
|
result = {"ok": False, "error": "Interrupted by user."}
|
|
255
384
|
except Exception as failure: # one structured failure, without a traceback
|
|
256
385
|
result = {"ok": False, "error": f"{type(failure).__name__}: {failure}"}
|
|
257
|
-
|
|
386
|
+
if args.command == "models":
|
|
387
|
+
label = f"models {args.models_command}"
|
|
388
|
+
elif args.command == "project":
|
|
389
|
+
label = f"project {args.project_command}"
|
|
390
|
+
else:
|
|
391
|
+
label = args.command
|
|
258
392
|
ui.result(result, label, time.monotonic() - started)
|
|
259
393
|
return 130 if interrupted else 0 if result["ok"] else 1
|
|
260
394
|
|
|
@@ -159,6 +159,33 @@ class Terminal:
|
|
|
159
159
|
return
|
|
160
160
|
if 'kinds' in result:
|
|
161
161
|
rows = [(item['kind'], f"{item['command']}\n{item['does']}") for item in result['kinds']]
|
|
162
|
+
elif 'project' in result and 'workspace' in result:
|
|
163
|
+
rows = []
|
|
164
|
+
if result.get('id'):
|
|
165
|
+
rows.append(('Id', result['id']))
|
|
166
|
+
if result.get('name'):
|
|
167
|
+
rows.append(('Name', result['name']))
|
|
168
|
+
rows += [('Project', result['project']), ('Workspace', result['workspace'] or 'Missing')]
|
|
169
|
+
rows.append(('Libraries', '\n'.join(result.get('models') or []) or 'None'))
|
|
170
|
+
rows.append(('Config', ', '.join(result.get('config') or []) or 'None'))
|
|
171
|
+
if result.get('registry'):
|
|
172
|
+
rows.append(('Registry', result['registry']))
|
|
173
|
+
if elapsed is not None:
|
|
174
|
+
rows.append(('Elapsed', f'{elapsed:.2f}s'))
|
|
175
|
+
elif result.get('registry') and isinstance(result.get('projects'), list):
|
|
176
|
+
rows = [('Registry', result['registry'])]
|
|
177
|
+
if not result['projects']:
|
|
178
|
+
rows.append(('Projects', 'None'))
|
|
179
|
+
for item in result['projects']:
|
|
180
|
+
state = 'ready' if item.get('ready') else 'not ready'
|
|
181
|
+
rows.append((item['id'], f"{item.get('name') or item['id']}\n{item.get('path', '')}\n{state}"))
|
|
182
|
+
elif result.get('registry') and result.get('id'):
|
|
183
|
+
rows = [('Id', result['id'])]
|
|
184
|
+
if result.get('name'):
|
|
185
|
+
rows.append(('Name', result['name']))
|
|
186
|
+
if result.get('path'):
|
|
187
|
+
rows.append(('Path', result['path']))
|
|
188
|
+
rows.append(('Registry', result['registry']))
|
|
162
189
|
elif isinstance(result.get('models'), list) and 'libraries' in result:
|
|
163
190
|
rows = [('Library', place) for place in result['libraries']]
|
|
164
191
|
rows += [(f'{index:02d}', f"{item['id']}\n{item['producer']} / {item['products']} products / {item['created_at'][:10]}")
|
|
@@ -195,7 +222,9 @@ class Terminal:
|
|
|
195
222
|
rows.append(('Report', str(Path(output) / 'report.html')))
|
|
196
223
|
if elapsed is not None:
|
|
197
224
|
rows.append(('Elapsed', f'{elapsed:.2f}s'))
|
|
198
|
-
titles = {'validate': 'VALID YAML', 'kinds': 'YAML KINDS',
|
|
225
|
+
titles = {'validate': 'VALID YAML', 'kinds': 'YAML KINDS',
|
|
226
|
+
'project list': 'PROJECTS', 'project add': 'PROJECT ADDED', 'project remove': 'PROJECT REMOVED',
|
|
227
|
+
'models list': 'MODEL LIBRARY',
|
|
199
228
|
'models show': 'MODEL DETAILS', 'models verify': 'MODEL VERIFIED',
|
|
200
229
|
'models import': 'MODEL IMPORTED', 'models export': 'MODEL EXPORTED'}
|
|
201
230
|
self.write(self.panel(titles.get(command, f'{command.upper()} COMPLETE'), rows, color='32'), self.out)
|
|
@@ -67,11 +67,17 @@ class Pattern:
|
|
|
67
67
|
|
|
68
68
|
@classmethod
|
|
69
69
|
def from_dict(cls, payload: Mapping[str, Any]) -> "Pattern":
|
|
70
|
+
if not isinstance(payload, Mapping):
|
|
71
|
+
raise TypeError("a pattern must be a mapping")
|
|
70
72
|
allowed = {"name", "products", "process_speed", "hourly_labor_cost", "min_carcasses",
|
|
71
73
|
"max_carcasses", "prices"}
|
|
72
74
|
unknown = sorted(set(payload) - allowed)
|
|
73
75
|
if unknown:
|
|
74
76
|
raise ValueError(f"pattern {payload.get('name')!r}: unknown keys={unknown}; allowed={sorted(allowed)}")
|
|
77
|
+
required = ("name", "products", "process_speed", "hourly_labor_cost")
|
|
78
|
+
missing = [key for key in required if key not in payload]
|
|
79
|
+
if missing:
|
|
80
|
+
raise ValueError(f"pattern {payload.get('name', '?')!r}: missing keys={missing}")
|
|
75
81
|
return cls(
|
|
76
82
|
name=str(payload["name"]),
|
|
77
83
|
products=tuple(str(item) for item in payload["products"]),
|
|
@@ -138,6 +144,8 @@ class ScenarioSpec:
|
|
|
138
144
|
|
|
139
145
|
@classmethod
|
|
140
146
|
def from_dict(cls, payload: Mapping[str, Any]) -> "ScenarioSpec":
|
|
147
|
+
if not isinstance(payload, Mapping):
|
|
148
|
+
raise TypeError("scenario must be a mapping")
|
|
141
149
|
allowed = {
|
|
142
150
|
"patterns", "prices", "pieces_per_carcass", "purchase_price_column", "purchase_price",
|
|
143
151
|
"weight_column", "available_column", "uncovered_products", "assignment",
|
|
@@ -145,6 +153,9 @@ class ScenarioSpec:
|
|
|
145
153
|
unknown = sorted(set(payload) - allowed)
|
|
146
154
|
if unknown:
|
|
147
155
|
raise ValueError(f"scenario: unknown keys={unknown}; allowed={sorted(allowed)}")
|
|
156
|
+
missing = [key for key in ("patterns", "prices") if key not in payload]
|
|
157
|
+
if missing:
|
|
158
|
+
raise ValueError(f"scenario: missing keys={missing}")
|
|
148
159
|
return cls(
|
|
149
160
|
patterns=tuple(Pattern.from_dict(item) for item in payload["patterns"]),
|
|
150
161
|
prices={str(key): float(value) for key, value in dict(payload["prices"]).items()},
|
|
@@ -381,6 +392,63 @@ def _plain(value: Any) -> Any:
|
|
|
381
392
|
return value.item() if hasattr(value, "item") else value
|
|
382
393
|
|
|
383
394
|
|
|
395
|
+
def random_assignment_comparison(
|
|
396
|
+
inputs: CuttingPlanInputs,
|
|
397
|
+
assignment: pd.DataFrame | None,
|
|
398
|
+
) -> tuple[dict[str, Any], pd.DataFrame]:
|
|
399
|
+
"""What uniform random cutting returns, beside the solved plan.
|
|
400
|
+
|
|
401
|
+
Each available carcass independently takes one pattern with equal probability. That is the
|
|
402
|
+
expectation, not one shuffled draw. A pattern minimum or maximum is not applied: those limits
|
|
403
|
+
couple the carcasses, and this number is the mean of the unrestricted draws. Purchase is paid
|
|
404
|
+
for every carcass, because a random cut still processes it. Profit, income and labour are the
|
|
405
|
+
per-carcass figures times how many carcasses the row stands for.
|
|
406
|
+
"""
|
|
407
|
+
profit = inputs.profit
|
|
408
|
+
patterns = list(profit.columns)
|
|
409
|
+
if not patterns:
|
|
410
|
+
raise ValueError("a random comparison needs at least one pattern")
|
|
411
|
+
available = inputs.available.reindex(profit.index).astype(float)
|
|
412
|
+
expected_profit = profit.mean(axis=1) * available
|
|
413
|
+
expected_income = inputs.income.reindex(columns=patterns).mean(axis=1) * available
|
|
414
|
+
expected_labor = inputs.labor_cost.reindex(columns=patterns).mean(axis=1) * available
|
|
415
|
+
expected_purchase = inputs.purchase_cost.reindex(profit.index).astype(float) * available
|
|
416
|
+
limits = any(
|
|
417
|
+
pattern.min_carcasses is not None or pattern.max_carcasses is not None
|
|
418
|
+
for pattern in inputs.patterns
|
|
419
|
+
)
|
|
420
|
+
solved = assignment is not None
|
|
421
|
+
if solved:
|
|
422
|
+
counts = assignment.reindex(index=profit.index, columns=patterns).fillna(0.0).astype(float)
|
|
423
|
+
chosen_profit = (profit * counts).sum(axis=1)
|
|
424
|
+
advantage = chosen_profit - expected_profit
|
|
425
|
+
else:
|
|
426
|
+
chosen_profit = pd.Series(np.nan, index=profit.index)
|
|
427
|
+
advantage = pd.Series(np.nan, index=profit.index)
|
|
428
|
+
by_carcass = pd.DataFrame(
|
|
429
|
+
{
|
|
430
|
+
"available": available.to_numpy(),
|
|
431
|
+
"expected_profit": expected_profit.to_numpy(),
|
|
432
|
+
"chosen_profit": chosen_profit.to_numpy(),
|
|
433
|
+
"advantage": advantage.to_numpy(),
|
|
434
|
+
},
|
|
435
|
+
index=profit.index,
|
|
436
|
+
)
|
|
437
|
+
total_carcasses = float(available.sum())
|
|
438
|
+
report = {
|
|
439
|
+
"rule": "each available carcass independently and uniformly among the patterns",
|
|
440
|
+
"patterns": len(patterns),
|
|
441
|
+
"respects_pattern_limits": not limits,
|
|
442
|
+
"expected_profit": float(expected_profit.sum()),
|
|
443
|
+
"expected_income": float(expected_income.sum()),
|
|
444
|
+
"expected_labor_cost": float(expected_labor.sum()),
|
|
445
|
+
"expected_purchase_cost": float(expected_purchase.sum()),
|
|
446
|
+
"expected_carcasses_per_pattern": total_carcasses / len(patterns),
|
|
447
|
+
"advantage_over_random": None if not solved else float(advantage.sum()),
|
|
448
|
+
}
|
|
449
|
+
return report, by_carcass
|
|
450
|
+
|
|
451
|
+
|
|
384
452
|
def solve(inputs: CuttingPlanInputs) -> PlanResult:
|
|
385
453
|
"""Assign carcasses to patterns: a mixed-integer problem (one pattern per carcass, pattern
|
|
386
454
|
capacities), solved exactly by HiGHS."""
|
|
@@ -31,7 +31,9 @@ from beefcot import runtime
|
|
|
31
31
|
from beefcot.model_format.library import LibraryError, ModelLibrary
|
|
32
32
|
from beefcot.runtime import MANIFEST as MANIFEST_NAME
|
|
33
33
|
from beefcot.runtime import Model
|
|
34
|
-
from beefcot.cutting_plan.economics import
|
|
34
|
+
from beefcot.cutting_plan.economics import (
|
|
35
|
+
SOLVER, CuttingPlanInputs, ScenarioSpec, build_inputs, random_assignment_comparison, solve,
|
|
36
|
+
)
|
|
35
37
|
from beefcot.yamlio import block, fresh_dir, from_yaml, load_yaml
|
|
36
38
|
|
|
37
39
|
KIND = "cutting_plan"
|
|
@@ -278,6 +280,15 @@ def run(config: str | Path) -> Path:
|
|
|
278
280
|
f"they cover as much as by value; compare patterns that break up the same cuts, or "
|
|
279
281
|
f"supply the rest with uncovered_products: external"
|
|
280
282
|
)
|
|
283
|
+
random_report, random_by_carcass = random_assignment_comparison(
|
|
284
|
+
inputs, result.assignment if solved else None
|
|
285
|
+
)
|
|
286
|
+
random_by_carcass.to_csv(root / "random_comparison.csv", index_label="carcass")
|
|
287
|
+
if not random_report["respects_pattern_limits"]:
|
|
288
|
+
warnings.append(
|
|
289
|
+
"the random expectation assigns every carcass uniformly and does not enforce a pattern's "
|
|
290
|
+
"minimum or maximum; the advantage is against that unrestricted draw"
|
|
291
|
+
)
|
|
281
292
|
unusual = inputs.provenance.get("unusual_inputs", {})
|
|
282
293
|
for column, entry in unusual.items():
|
|
283
294
|
warnings.append(
|
|
@@ -293,6 +304,7 @@ def run(config: str | Path) -> Path:
|
|
|
293
304
|
"solver": result.solver,
|
|
294
305
|
"seconds": result.seconds,
|
|
295
306
|
"objective_profit": result.objective,
|
|
307
|
+
"random_assignment": random_report,
|
|
296
308
|
"carcasses": int(len(carcasses)),
|
|
297
309
|
"carcasses_assigned": int(assigned.to_numpy().sum()),
|
|
298
310
|
"per_pattern": per_pattern,
|
|
@@ -521,8 +521,17 @@ def _fit_lstsq(adapter: "NodeRegressorAdapter", X: np.ndarray, y: np.ndarray) ->
|
|
|
521
521
|
adapter.beta = linalg.lstsq(X, y)[0]
|
|
522
522
|
|
|
523
523
|
|
|
524
|
+
class NNLSConvergenceError(ValueError):
|
|
525
|
+
"""A known, non-computable candidate fit; other runtime errors still propagate."""
|
|
526
|
+
|
|
527
|
+
|
|
524
528
|
def _fit_nnls(adapter: "NodeRegressorAdapter", X: np.ndarray, y: np.ndarray) -> None:
|
|
525
|
-
|
|
529
|
+
try:
|
|
530
|
+
adapter.beta = optimize.nnls(X, y)[0]
|
|
531
|
+
except RuntimeError as exc:
|
|
532
|
+
if str(exc) != "Maximum number of iterations reached.":
|
|
533
|
+
raise
|
|
534
|
+
raise NNLSConvergenceError(f"nnls convergence failure: {exc}") from exc
|
|
526
535
|
|
|
527
536
|
|
|
528
537
|
def _fit_robust(adapter: "NodeRegressorAdapter", X: np.ndarray, y: np.ndarray) -> None:
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import warnings
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
+
from functools import lru_cache
|
|
14
15
|
from typing import Sequence
|
|
15
16
|
|
|
16
17
|
import numpy as np
|
|
@@ -180,8 +181,21 @@ def _boxcox_lambda(values: np.ndarray, bounds: tuple[float, float] = (-2.0, 2.0)
|
|
|
180
181
|
directly rather than reimplemented, and clamped because an unbounded lambda
|
|
181
182
|
on 190 rows can wander far enough to make the inverse overflow.
|
|
182
183
|
"""
|
|
184
|
+
values = np.asarray(values, dtype=float)
|
|
185
|
+
# The response and its fold, not the input mapping or learner, determine
|
|
186
|
+
# lambda. Key by every training value (in order) and both bounds: held-out
|
|
187
|
+
# data, other folds and other targets can never share fitted state by name.
|
|
188
|
+
# Bytes also bound the retained data without keeping whole frames alive.
|
|
189
|
+
return _boxcox_lambda_cached(values.tobytes(), tuple(bounds))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@lru_cache(maxsize=128)
|
|
193
|
+
def _boxcox_lambda_cached(training_values: bytes, bounds: tuple[float, float]) -> float:
|
|
194
|
+
"""Process-local, bounded memoization of the unchanged deterministic fit."""
|
|
183
195
|
from scipy import stats
|
|
184
196
|
|
|
197
|
+
values = np.frombuffer(training_values, dtype=float)
|
|
198
|
+
|
|
185
199
|
try:
|
|
186
200
|
lam = float(stats.boxcox_normmax(np.asarray(values, dtype=float), brack=bounds))
|
|
187
201
|
except (ValueError, ArithmeticError, RuntimeError): # pragma: no cover - degenerate response
|
|
@@ -108,7 +108,7 @@ def flat_tree(tree: Mapping[str, Any]) -> dict[str, list]:
|
|
|
108
108
|
return flat
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def network_block(fitted, products: Sequence[str]) -> dict[str, Any]:
|
|
111
|
+
def network_block(fitted, products: Sequence[str], *, selected: Sequence[str] | None = None) -> dict[str, Any]:
|
|
112
112
|
"""A shared network (``beefcot.producers.network.FittedNetwork``) as one block: its design, its
|
|
113
113
|
members' weights, one output per product, each put back in its product's unit by the
|
|
114
114
|
standardisation the targets were fitted in."""
|
|
@@ -116,7 +116,7 @@ def network_block(fitted, products: Sequence[str]) -> dict[str, Any]:
|
|
|
116
116
|
mean, scale = [float(v) for v in fitted.target_mean], [float(v) for v in fitted.target_scale]
|
|
117
117
|
if list(fitted.outputs) != list(products) or len(mean) != len(products):
|
|
118
118
|
raise ValueError(f"the network gives {list(fitted.outputs)}, not {list(products)}")
|
|
119
|
-
|
|
119
|
+
block = {
|
|
120
120
|
"terms": terms_of(design),
|
|
121
121
|
"center": list(design.center),
|
|
122
122
|
"scale": list(design.scale),
|
|
@@ -135,3 +135,13 @@ def network_block(fitted, products: Sequence[str]) -> dict[str, Any]:
|
|
|
135
135
|
for i, code in enumerate(products)
|
|
136
136
|
],
|
|
137
137
|
}
|
|
138
|
+
if selected is not None:
|
|
139
|
+
selected = list(selected)
|
|
140
|
+
if not selected or len(set(selected)) != len(selected) or not set(selected) <= set(products):
|
|
141
|
+
raise ValueError("selected network outputs must be distinct declared products")
|
|
142
|
+
indices = [list(products).index(code) for code in selected]
|
|
143
|
+
block["outputs"] = [block["outputs"][i] for i in indices]
|
|
144
|
+
for member in block["kernel"]["members"]:
|
|
145
|
+
member["weights"][-1] = [[row[i] for i in indices] for row in member["weights"][-1]]
|
|
146
|
+
member["biases"][-1] = [member["biases"][-1][i] for i in indices]
|
|
147
|
+
return block
|