repyability 0.4.0__tar.gz → 0.5.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 (88) hide show
  1. repyability-0.5.0/PKG-INFO +118 -0
  2. repyability-0.4.0/repyability.egg-info/PKG-INFO → repyability-0.5.0/README.md +20 -15
  3. repyability-0.5.0/pyproject.toml +112 -0
  4. repyability-0.5.0/repyability/__init__.py +55 -0
  5. repyability-0.5.0/repyability/_version.py +8 -0
  6. repyability-0.5.0/repyability/maintenance.py +28 -0
  7. repyability-0.5.0/repyability/non_repairable.py +227 -0
  8. repyability-0.5.0/repyability/rbd/_model_utils.py +59 -0
  9. {repyability-0.4.0 → repyability-0.5.0}/repyability/rbd/helper_classes.py +11 -21
  10. {repyability-0.4.0 → repyability-0.5.0}/repyability/rbd/min_path_sets.py +22 -45
  11. repyability-0.5.0/repyability/rbd/non_repairable_rbd.py +1006 -0
  12. repyability-0.5.0/repyability/rbd/numerical_convolution.py +191 -0
  13. repyability-0.5.0/repyability/rbd/rbd.py +997 -0
  14. repyability-0.5.0/repyability/rbd/rbd_graph.py +139 -0
  15. repyability-0.5.0/repyability/rbd/repairable_rbd.py +941 -0
  16. repyability-0.5.0/repyability/rbd/repeated_node.py +55 -0
  17. repyability-0.5.0/repyability/rbd/repeated_standby_node.py +71 -0
  18. repyability-0.5.0/repyability/rbd/results.py +278 -0
  19. repyability-0.5.0/repyability/rbd/serialisation.py +248 -0
  20. repyability-0.5.0/repyability/rbd/standby_node.py +191 -0
  21. repyability-0.5.0/repyability/repairable.py +190 -0
  22. repyability-0.5.0/repyability/tests/conftest.py +324 -0
  23. repyability-0.5.0/repyability/tests/test_api_standardisation.py +192 -0
  24. repyability-0.5.0/repyability/tests/test_bx_life.py +97 -0
  25. repyability-0.5.0/repyability/tests/test_fussell_vesely_deprecation.py +44 -0
  26. repyability-0.5.0/repyability/tests/test_helper_classes.py +21 -0
  27. repyability-0.5.0/repyability/tests/test_model_utils.py +53 -0
  28. repyability-0.5.0/repyability/tests/test_non_repairable.py +173 -0
  29. repyability-0.5.0/repyability/tests/test_rbd_allocation.py +65 -0
  30. repyability-0.5.0/repyability/tests/test_rbd_analytic_solvability.py +79 -0
  31. repyability-0.5.0/repyability/tests/test_rbd_berge_cut_sets.py +126 -0
  32. repyability-0.5.0/repyability/tests/test_rbd_exact_probability.py +104 -0
  33. repyability-0.5.0/repyability/tests/test_rbd_exponential_standby.py +76 -0
  34. {repyability-0.4.0 → repyability-0.5.0}/repyability/tests/test_rbd_get_min_path_sets.py +2 -1
  35. repyability-0.5.0/repyability/tests/test_rbd_hazard.py +55 -0
  36. repyability-0.5.0/repyability/tests/test_rbd_importances.py +620 -0
  37. repyability-0.5.0/repyability/tests/test_rbd_koon.py +244 -0
  38. repyability-0.5.0/repyability/tests/test_rbd_misc.py +82 -0
  39. repyability-0.5.0/repyability/tests/test_rbd_non_repairable.py +244 -0
  40. repyability-0.5.0/repyability/tests/test_rbd_numerical_convolution.py +76 -0
  41. repyability-0.5.0/repyability/tests/test_rbd_repeated_standby.py +53 -0
  42. repyability-0.5.0/repyability/tests/test_rbd_set_working_failed.py +251 -0
  43. repyability-0.5.0/repyability/tests/test_rbd_sf.py +166 -0
  44. repyability-0.5.0/repyability/tests/test_rbd_structure.py +144 -0
  45. repyability-0.5.0/repyability/tests/test_rbd_switching_probability.py +96 -0
  46. repyability-0.5.0/repyability/tests/test_repairable.py +134 -0
  47. repyability-0.5.0/repyability/tests/test_repairable_accounting.py +50 -0
  48. repyability-0.5.0/repyability/tests/test_repairable_metrics.py +125 -0
  49. {repyability-0.4.0 → repyability-0.5.0}/repyability/tests/test_repairable_rbd.py +51 -51
  50. repyability-0.5.0/repyability/tests/test_repeated_node.py +62 -0
  51. repyability-0.5.0/repyability/tests/test_results.py +60 -0
  52. repyability-0.5.0/repyability/tests/test_rng_reproducibility.py +89 -0
  53. repyability-0.5.0/repyability/tests/test_serialisation.py +140 -0
  54. repyability-0.5.0/repyability/tests/test_simulation_validation.py +162 -0
  55. repyability-0.5.0/repyability/tests/test_standby.py +64 -0
  56. repyability-0.5.0/repyability/utils/wrappers.py +86 -0
  57. repyability-0.5.0/repyability.egg-info/PKG-INFO +118 -0
  58. repyability-0.5.0/repyability.egg-info/SOURCES.txt +64 -0
  59. {repyability-0.4.0 → repyability-0.5.0}/repyability.egg-info/dependency_links.txt +0 -0
  60. repyability-0.5.0/repyability.egg-info/requires.txt +20 -0
  61. {repyability-0.4.0 → repyability-0.5.0}/repyability.egg-info/top_level.txt +0 -0
  62. repyability-0.4.0/PKG-INFO +0 -51
  63. repyability-0.4.0/README.md +0 -37
  64. repyability-0.4.0/pyproject.toml +0 -15
  65. repyability-0.4.0/repyability/__init__.py +0 -2
  66. repyability-0.4.0/repyability/non_repairable.py +0 -51
  67. repyability-0.4.0/repyability/rbd/min_cut_sets.py +0 -71
  68. repyability-0.4.0/repyability/rbd/rbd.py +0 -613
  69. repyability-0.4.0/repyability/rbd/rbd_args_check.py +0 -53
  70. repyability-0.4.0/repyability/rbd/rbd_graph.py +0 -26
  71. repyability-0.4.0/repyability/rbd/repairable_rbd.py +0 -262
  72. repyability-0.4.0/repyability/repairable.py +0 -28
  73. repyability-0.4.0/repyability/tests/conftest.py +0 -429
  74. repyability-0.4.0/repyability/tests/fixed_probability.py +0 -17
  75. repyability-0.4.0/repyability/tests/test_non_repairable.py +0 -39
  76. repyability-0.4.0/repyability/tests/test_rbd_importances.py +0 -677
  77. repyability-0.4.0/repyability/tests/test_rbd_koon.py +0 -177
  78. repyability-0.4.0/repyability/tests/test_rbd_misc.py +0 -85
  79. repyability-0.4.0/repyability/tests/test_rbd_sf.py +0 -183
  80. repyability-0.4.0/repyability/tests/test_repairable.py +0 -13
  81. repyability-0.4.0/repyability.egg-info/SOURCES.txt +0 -32
  82. repyability-0.4.0/repyability.egg-info/requires.txt +0 -4
  83. repyability-0.4.0/setup.py +0 -23
  84. {repyability-0.4.0 → repyability-0.5.0}/LICENSE +0 -0
  85. {repyability-0.4.0 → repyability-0.5.0}/repyability/rbd/__init__.py +0 -0
  86. {repyability-0.4.0 → repyability-0.5.0}/repyability/tests/__init__.py +0 -0
  87. {repyability-0.4.0 → repyability-0.5.0}/repyability/tests/test_rbd_get_min_cut_sets.py +0 -0
  88. {repyability-0.4.0 → repyability-0.5.0}/setup.cfg +0 -0
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: repyability
3
+ Version: 0.5.0
4
+ Summary: Reliability engineering tools for Python
5
+ Author-email: derrynknife <derrynknife@gmail.com>
6
+ License: Copyright (c) 2020 The Python Packaging Authority
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ Project-URL: Homepage, https://github.com/derrynknife/RePyability
26
+ Project-URL: Documentation, https://derrynknife.github.io/RePyability/
27
+ Project-URL: Repository, https://github.com/derrynknife/RePyability
28
+ Project-URL: Issues, https://github.com/derrynknife/RePyability/issues
29
+ Keywords: reliability,reliability-engineering,reliability-block-diagram,rbd,survival-analysis,availability,maintainability
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Science/Research
32
+ Classifier: Intended Audience :: Education
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Scientific/Engineering
39
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
40
+ Requires-Python: >=3.11
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: numpy<3,>=2.0
44
+ Requires-Dist: scipy>=1.7
45
+ Requires-Dist: networkx>=3.0
46
+ Requires-Dist: surpyval<0.12,>=0.11.1
47
+ Requires-Dist: tqdm>=4.64
48
+ Provides-Extra: dev
49
+ Requires-Dist: black==26.3.1; extra == "dev"
50
+ Requires-Dist: isort>=5.12; extra == "dev"
51
+ Requires-Dist: flake8>=6.0; extra == "dev"
52
+ Requires-Dist: flake8-pyproject>=1.2; extra == "dev"
53
+ Requires-Dist: mypy>=1.8; extra == "dev"
54
+ Requires-Dist: pytest>=7.0; extra == "dev"
55
+ Requires-Dist: coverage>=7.0; extra == "dev"
56
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
57
+ Requires-Dist: joblib>=1.0; extra == "dev"
58
+ Provides-Extra: docs
59
+ Requires-Dist: mkdocs-material>=9.0; extra == "docs"
60
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
61
+ Dynamic: license-file
62
+
63
+ # RePyability
64
+
65
+ [![actions](https://github.com/derrynknife/RePyability/actions/workflows/actions.yml/badge.svg)](https://github.com/derrynknife/RePyability/actions/workflows/actions.yml)
66
+ [![docs](https://github.com/derrynknife/RePyability/actions/workflows/docs.yml/badge.svg)](https://derrynknife.github.io/RePyability/)
67
+
68
+ Reliability Engineering Tools
69
+
70
+ This is a series of tools created to make an open source set of methods to be used by reliability engineers to make it more accessible for students right through to practicing professionals.
71
+
72
+ ## Install
73
+ RePyability can be installed via pip using the PyPI [repository](https://pypi.org/project/repyability/)
74
+
75
+ ```bash
76
+ pip install repyability
77
+ ```
78
+
79
+ ## Documentation
80
+ The full documentation — overview, user guide, and API reference — is hosted at
81
+ **[derrynknife.github.io/RePyability](https://derrynknife.github.io/RePyability/)**.
82
+
83
+ It is built with [MkDocs](https://www.mkdocs.org/) from the sources in `docs/`
84
+ and `mkdocs.yml`, and published to GitHub Pages on every push to `master`. To
85
+ build and serve it locally:
86
+
87
+ ```bash
88
+ pip install -e .[docs]
89
+ mkdocs serve # then open http://127.0.0.1:8000
90
+ ```
91
+
92
+ The source lives in `docs/` and `mkdocs.yml`; start with `docs/index.md`.
93
+
94
+ ## Testing
95
+ Run the testing suite by simply executing:
96
+ ```bash
97
+ pytest
98
+ ```
99
+ or use coverage to get a coverage report:
100
+ ```bash
101
+ coverage run -m pytest # Run pytest under coverage's watch
102
+ coverage report # Print coverage report
103
+ coverage html # Make a html coverage report (really useful), open htmlcov/index.html
104
+ ```
105
+
106
+ ## Pre-commit
107
+ ### TL;DR
108
+ - Pip install `pre-commit` (it's in `requirements_dev.txt` anyways)
109
+ - Run `pre-commit install` which sets up the git hook scripts
110
+ - If you'd like, run `pre-commit run --all-files` to run the hooks on all files
111
+ - When you go to commit, it will only proceed after all the hooks succeed
112
+
113
+ ### Why?
114
+ To ensure the good code quality and consistency it is recommended that when contributing to this
115
+ repository to use the provided `.pre-commit-config.yaml` configuration for the Python package
116
+ `pre-commit` (https://pre-commit.com). Upon making a commit, it checks that imports
117
+ and requirements are sorted, syntax is up-to-date, code is formatted, linted, and statically type-checked,
118
+ all with the same tools and configurations as one another.
@@ -1,18 +1,8 @@
1
- Metadata-Version: 2.1
2
- Name: repyability
3
- Version: 0.4.0
4
- Summary: A reliability analysis python package
5
- Home-page: https://github.com/derrynknife/RePyability
6
- Author: Derryn Knife
7
- Author-email: derryn@reliafy.com
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
-
15
1
  # RePyability
2
+
3
+ [![actions](https://github.com/derrynknife/RePyability/actions/workflows/actions.yml/badge.svg)](https://github.com/derrynknife/RePyability/actions/workflows/actions.yml)
4
+ [![docs](https://github.com/derrynknife/RePyability/actions/workflows/docs.yml/badge.svg)](https://derrynknife.github.io/RePyability/)
5
+
16
6
  Reliability Engineering Tools
17
7
 
18
8
  This is a series of tools created to make an open source set of methods to be used by reliability engineers to make it more accessible for students right through to practicing professionals.
@@ -24,6 +14,21 @@ RePyability can be installed via pip using the PyPI [repository](https://pypi.or
24
14
  pip install repyability
25
15
  ```
26
16
 
17
+ ## Documentation
18
+ The full documentation — overview, user guide, and API reference — is hosted at
19
+ **[derrynknife.github.io/RePyability](https://derrynknife.github.io/RePyability/)**.
20
+
21
+ It is built with [MkDocs](https://www.mkdocs.org/) from the sources in `docs/`
22
+ and `mkdocs.yml`, and published to GitHub Pages on every push to `master`. To
23
+ build and serve it locally:
24
+
25
+ ```bash
26
+ pip install -e .[docs]
27
+ mkdocs serve # then open http://127.0.0.1:8000
28
+ ```
29
+
30
+ The source lives in `docs/` and `mkdocs.yml`; start with `docs/index.md`.
31
+
27
32
  ## Testing
28
33
  Run the testing suite by simply executing:
29
34
  ```bash
@@ -38,7 +43,7 @@ coverage html # Make a html coverage report (really useful), open html
38
43
 
39
44
  ## Pre-commit
40
45
  ### TL;DR
41
- - Pip install `pre-commit` (it's in `requirements.txt` anyways)
46
+ - Pip install `pre-commit` (it's in `requirements_dev.txt` anyways)
42
47
  - Run `pre-commit install` which sets up the git hook scripts
43
48
  - If you'd like, run `pre-commit run --all-files` to run the hooks on all files
44
49
  - When you go to commit, it will only proceed after all the hooks succeed
@@ -0,0 +1,112 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "repyability"
7
+ dynamic = ["version"]
8
+ description = "Reliability engineering tools for Python"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "derrynknife", email = "derrynknife@gmail.com" }]
13
+ keywords = [
14
+ "reliability",
15
+ "reliability-engineering",
16
+ "reliability-block-diagram",
17
+ "rbd",
18
+ "survival-analysis",
19
+ "availability",
20
+ "maintainability",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Science/Research",
25
+ "Intended Audience :: Education",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: Scientific/Engineering",
32
+ "Topic :: Scientific/Engineering :: Mathematics",
33
+ ]
34
+ dependencies = [
35
+ "numpy>=2.0,<3",
36
+ "scipy>=1.7",
37
+ "networkx>=3.0",
38
+ "surpyval>=0.11.1,<0.12",
39
+ "tqdm>=4.64",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "black==26.3.1",
45
+ "isort>=5.12",
46
+ "flake8>=6.0",
47
+ "flake8-pyproject>=1.2",
48
+ "mypy>=1.8",
49
+ "pytest>=7.0",
50
+ "coverage>=7.0",
51
+ "pre-commit>=3.0",
52
+ # Testing-only: the suite imports surpyval, which transitively pulls in its
53
+ # RandomSurvivalForest (and hence joblib). Not a runtime dependency of
54
+ # RePyability.
55
+ "joblib>=1.0",
56
+ ]
57
+ docs = [
58
+ "mkdocs-material>=9.0",
59
+ "mkdocstrings[python]>=0.24",
60
+ ]
61
+
62
+ [project.urls]
63
+ Homepage = "https://github.com/derrynknife/RePyability"
64
+ Documentation = "https://derrynknife.github.io/RePyability/"
65
+ Repository = "https://github.com/derrynknife/RePyability"
66
+ Issues = "https://github.com/derrynknife/RePyability/issues"
67
+
68
+ [tool.setuptools.dynamic]
69
+ version = { attr = "repyability._version.__version__" }
70
+
71
+ [tool.setuptools.packages.find]
72
+ include = ["repyability*"]
73
+
74
+ # For Black pre-commit hook
75
+ [tool.black]
76
+ line-length = 79
77
+
78
+ # So isort is compatible with black
79
+ [tool.isort]
80
+ profile = "black"
81
+ line_length = 79
82
+
83
+ # mypy
84
+ [tool.mypy]
85
+ exclude = ['repyability/tests/']
86
+
87
+ [[tool.mypy.overrides]]
88
+ module = [
89
+ 'scipy.*',
90
+ 'networkx',
91
+ 'surpyval.*',
92
+ 'tqdm',
93
+ 'numpy',
94
+ 'numpy.*',
95
+ ]
96
+ ignore_missing_imports = true
97
+
98
+ # Coverage
99
+ [tool.coverage.run]
100
+ source = ["repyability"]
101
+ omit = ["*/tests/*"]
102
+
103
+ [tool.coverage.report]
104
+ # The library (excluding tests) must stay above this bar.
105
+ fail_under = 80
106
+
107
+ # Flake8
108
+ [tool.flake8]
109
+ extend-ignore = ['E203', 'W503']
110
+ per-file-ignores = [
111
+ '*/__init__.py:F401',
112
+ ]
@@ -0,0 +1,55 @@
1
+ """RePyability — reliability engineering tools for Python.
2
+
3
+ The most commonly used classes are re-exported here so they can be imported
4
+ directly from the top-level package, e.g.::
5
+
6
+ from repyability import NonRepairableRBD, StandbyModel
7
+ """
8
+
9
+ from repyability._version import __version__
10
+ from repyability.maintenance import MaintenancePolicy
11
+ from repyability.non_repairable import NonRepairable
12
+ from repyability.rbd.helper_classes import (
13
+ PerfectReliability,
14
+ PerfectUnreliability,
15
+ )
16
+ from repyability.rbd.non_repairable_rbd import NonRepairableRBD
17
+ from repyability.rbd.rbd import RBD
18
+ from repyability.rbd.repairable_rbd import RepairableRBD
19
+ from repyability.rbd.repeated_node import RepeatedNode
20
+ from repyability.rbd.repeated_standby_node import RepeatedStandbyNode
21
+ from repyability.rbd.results import (
22
+ AvailabilityResult,
23
+ ConfidenceInterval,
24
+ Criticalities,
25
+ FailureCriticalityIndex,
26
+ RestorationCriticalityIndex,
27
+ UpDownImportance,
28
+ )
29
+ from repyability.rbd.standby_node import StandbyModel
30
+ from repyability.repairable import Repairable
31
+
32
+ __all__ = [
33
+ "__version__",
34
+ # System models
35
+ "RBD",
36
+ "NonRepairableRBD",
37
+ "RepairableRBD",
38
+ # Component models
39
+ "NonRepairable",
40
+ "Repairable",
41
+ "StandbyModel",
42
+ "RepeatedNode",
43
+ "RepeatedStandbyNode",
44
+ # Helpers
45
+ "PerfectReliability",
46
+ "PerfectUnreliability",
47
+ # Result types
48
+ "AvailabilityResult",
49
+ "ConfidenceInterval",
50
+ "Criticalities",
51
+ "UpDownImportance",
52
+ "FailureCriticalityIndex",
53
+ "RestorationCriticalityIndex",
54
+ "MaintenancePolicy",
55
+ ]
@@ -0,0 +1,8 @@
1
+ """Single source of truth for the package version.
2
+
3
+ Kept deliberately free of imports so the build backend can read
4
+ ``__version__`` statically (without importing the package and its
5
+ dependencies) via ``[tool.setuptools.dynamic]`` in ``pyproject.toml``.
6
+ """
7
+
8
+ __version__ = "0.5.0"
@@ -0,0 +1,28 @@
1
+ """Shared result types for component maintenance-policy optimisations."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class MaintenancePolicy:
8
+ """An optimal preventive-maintenance policy for a single component.
9
+
10
+ Returned by ``NonRepairable.optimal_replacement_policy()`` (age
11
+ replacement) and ``Repairable.optimal_overhaul_policy()`` (overhaul
12
+ under minimal repair).
13
+
14
+ Attributes
15
+ ----------
16
+ interval : float
17
+ The optimal preventive interval: the replacement age for an
18
+ age-replacement policy, or the overhaul interval for a
19
+ minimal-repair component. ``inf`` when preventive action never
20
+ pays (run to failure / never overhaul).
21
+ cost_rate : float
22
+ The long-run cost per unit time under the policy. When
23
+ ``interval`` is ``inf`` this is the limiting cost rate of running
24
+ without preventive action.
25
+ """
26
+
27
+ interval: float
28
+ cost_rate: float
@@ -0,0 +1,227 @@
1
+ import numpy as np
2
+ from scipy.integrate import quad
3
+ from scipy.interpolate import interp1d
4
+ from scipy.optimize import minimize
5
+ from surpyval import ExactEventTime, NonParametric, Parametric
6
+
7
+ from repyability.maintenance import MaintenancePolicy
8
+ from repyability.rbd._model_utils import (
9
+ distribution_name,
10
+ is_exponential,
11
+ model_mean,
12
+ )
13
+ from repyability.rbd.standby_node import StandbyModel
14
+
15
+ FAILURE = 1
16
+ REPLACE = 0
17
+
18
+
19
+ class NonRepairable:
20
+ """A component renewed by replacement ("as good as new").
21
+
22
+ Pairs a lifetime (reliability) model with a time-to-replace model for
23
+ a unit that cannot be repaired in place: every failure or planned
24
+ replacement fits a new unit, so each cycle is a statistical renewal.
25
+
26
+ It plays two roles:
27
+
28
+ - Standalone, it prices the classic *age-replacement* policy — replace
29
+ preventively at age ``t`` (planned, cost ``cp``) or on failure
30
+ (unplanned, cost ``cu > cp``) — via ``find_optimal_replacement()``
31
+ and ``optimal_replacement_policy()``.
32
+ - Inside ``RepairableRBD``, it is the per-component representation
33
+ used by the availability engine (components are "repaired" by as-new
34
+ replacement, which is exactly the renewal this class models).
35
+
36
+ Contrast with ``Repairable``, which models *minimal repair* ("as bad
37
+ as old") and the overhaul-interval policy.
38
+ """
39
+
40
+ def __init__(
41
+ self, reliability, time_to_replace=ExactEventTime.from_params(0)
42
+ ):
43
+ if isinstance(reliability, Parametric):
44
+ self.model_parameterization = "parametric"
45
+ self.reliability_function = reliability.sf
46
+ elif isinstance(reliability, NonParametric):
47
+ # TODO: Allow non-interpolated?
48
+ self.model_parameterization = "non-parametric"
49
+ self.reliability_function = interp1d(
50
+ reliability.x, 1 - reliability.F, fill_value="extrapolate"
51
+ )
52
+ elif isinstance(reliability, StandbyModel):
53
+ self.model_parameterization = "non-parametric"
54
+ self.reliability_function = interp1d(
55
+ reliability.model.x,
56
+ 1 - reliability.model.F,
57
+ fill_value="extrapolate",
58
+ )
59
+ else:
60
+ raise ValueError("Unknown reliability function")
61
+
62
+ self.reliability = reliability
63
+ self.time_to_replace = time_to_replace
64
+ self.cost_rate = np.vectorize(
65
+ self._cost_rate,
66
+ doc=(
67
+ "Long-run cost per unit time of an age-replacement policy "
68
+ "with replacement age t:\n"
69
+ "(cp * R(t) + cu * F(t)) / integral_0^t R(u) du.\n"
70
+ "Vectorised over t."
71
+ ),
72
+ )
73
+ self.__next_event_type = FAILURE
74
+
75
+ def set_costs_planned_and_unplanned(self, cp, cu):
76
+ if cp >= cu:
77
+ raise ValueError("Planned costs must be less than unplanned costs")
78
+ self.cp = cp
79
+ self.cu = cu
80
+
81
+ def avg_replacement_time(self, t):
82
+ if self.model_parameterization == "parametric":
83
+ out = quad(self.reliability_function, 0, t)[0]
84
+ else:
85
+ mask = self.reliability.x < t
86
+ x_less_than_t = self.reliability.x[mask]
87
+ dt = np.diff(x_less_than_t)
88
+ F = self.reliability_function(x_less_than_t[1:])
89
+ out = (dt * F).sum()
90
+
91
+ return out
92
+
93
+ def _cost_rate(self, t):
94
+ # surpyval >= 0.11 returns 1-element arrays from sf/mean/qf, and t can
95
+ # arrive as a 1-element array from scipy.optimize.minimize. Coerce to a
96
+ # plain float so the quadrature bound in avg_replacement_time() is a
97
+ # scalar (scipy.integrate.quad rejects array bounds).
98
+ t = float(np.asarray(t).item())
99
+ planned_costs = self.reliability_function(t) * self.cp
100
+ unplanned_costs = (1 - self.reliability_function(t)) * self.cu
101
+ avg_repl_time = self.avg_replacement_time(t)
102
+ return (planned_costs + unplanned_costs) / avg_repl_time
103
+
104
+ def _log_cost_rate(self, t):
105
+ return np.log(self._cost_rate(t))
106
+
107
+ def mean_unavailability(self) -> float:
108
+ return 1 - self.mean_availability()
109
+
110
+ def mean_availability(self) -> float:
111
+ """Long-run availability, MTTF / (MTTF + MTTR)."""
112
+ if isinstance(self.reliability, NonParametric):
113
+ raise ValueError(
114
+ "Mean Availability requires a parametric reliability model"
115
+ )
116
+ mttf = model_mean(self.reliability)
117
+ mttr = model_mean(self.time_to_replace)
118
+ return mttf / (mttr + mttf)
119
+
120
+ def failure_frequency(self) -> float:
121
+ """Long-run failure frequency (failures per unit time).
122
+
123
+ For an alternating renewal process (fail, repair, fail, ...) this is
124
+ ``1 / (MTTF + MTTR)`` — one failure per mean up-down cycle.
125
+ """
126
+ if isinstance(self.reliability, NonParametric):
127
+ raise ValueError(
128
+ "Failure frequency requires a parametric reliability model"
129
+ )
130
+ mttf = model_mean(self.reliability)
131
+ mttr = model_mean(self.time_to_replace)
132
+ return 1.0 / (mttf + mttr)
133
+
134
+ def _cost_rate_with_log_x(self, x):
135
+ return self._cost_rate(np.exp(x))
136
+
137
+ def _log_cost_rate_with_log_x(self, x):
138
+ return self._cost_rate(np.exp(x))
139
+
140
+ def find_optimal_replacement(self, options=None):
141
+ if self.model_parameterization == "parametric":
142
+ if is_exponential(self.reliability) and not (
143
+ getattr(self.reliability, "offset", False)
144
+ or getattr(self.reliability, "zi", False)
145
+ or getattr(self.reliability, "lfp", False)
146
+ ):
147
+ # Memoryless lifetime: an old unit is statistically as good
148
+ # as a new one, so preventive replacement never pays.
149
+ return np.inf
150
+ if distribution_name(self.reliability) == "Weibull":
151
+ if self.reliability.offset:
152
+ pass
153
+ elif self.reliability.zi:
154
+ pass
155
+ elif self.reliability.lfp:
156
+ pass
157
+ elif self.reliability.params[1] <= 1:
158
+ return np.inf
159
+ # When using a parametric distribution the optimisation is
160
+ # straight forward. Simply find the point in the support where
161
+ # the cost rate is minimised. Uses quadrature to integrate!
162
+ mean = self.reliability.mean()
163
+ old_err_state = np.seterr(all="ignore")
164
+ res = minimize(self._cost_rate_with_log_x, np.log(mean), tol=1e-10)
165
+ res_log = minimize(
166
+ self._log_cost_rate_with_log_x, np.log(mean), tol=1e-10
167
+ )
168
+ np.seterr(**old_err_state)
169
+ self.optimisation_results = res
170
+ if res["fun"] < np.exp(res_log["fun"]):
171
+ optimal = np.exp(res.x[0])
172
+ else:
173
+ optimal = np.exp(res_log.x[0])
174
+ else:
175
+ # When using non-parametric estimations, it can also be straight
176
+ # forward. Simply find the cost rate at a number of places
177
+ # from the min to the max support of the model and return the
178
+ # value of x which has the minimum cost rate.
179
+ x_search = np.linspace(
180
+ self.reliability.x.min(), self.reliability.x.max(), 10000
181
+ )
182
+
183
+ dt = np.diff(x_search, prepend=0)
184
+ R = self.reliability_function(x_search)
185
+ avg_replacement_times = (dt * R).cumsum()
186
+
187
+ planned_costs = R * self.cp
188
+ unplanned_costs = (1 - R) * self.cu
189
+
190
+ costs = (planned_costs + unplanned_costs) / avg_replacement_times
191
+
192
+ optimal_idx = np.argmin(costs)
193
+ optimal = x_search[optimal_idx]
194
+ return optimal
195
+
196
+ def optimal_replacement_policy(self) -> MaintenancePolicy:
197
+ """The optimal age-replacement policy as a typed result.
198
+
199
+ Returns a ``MaintenancePolicy`` whose ``interval`` is the
200
+ cost-optimal replacement age and whose ``cost_rate`` is the
201
+ long-run cost per unit time under it. When preventive replacement
202
+ never pays (no aging — e.g. an exponential lifetime, or a Weibull
203
+ with shape <= 1) the interval is ``inf`` and the cost rate is the
204
+ run-to-failure rate ``cu / MTTF``.
205
+ """
206
+ if not hasattr(self, "cp"):
207
+ raise ValueError(
208
+ "costs not set: call set_costs_planned_and_unplanned"
209
+ "(cp, cu) first"
210
+ )
211
+ interval = self.find_optimal_replacement()
212
+ if np.isinf(interval):
213
+ rate = self.cu / model_mean(self.reliability)
214
+ else:
215
+ rate = float(self._cost_rate(interval))
216
+ return MaintenancePolicy(interval=float(interval), cost_rate=rate)
217
+
218
+ def reset(self):
219
+ self.__next_event_type = FAILURE
220
+
221
+ def next_event(self):
222
+ if self.__next_event_type == FAILURE:
223
+ self.__next_event_type = REPLACE
224
+ return self.reliability.random(1).item(), False
225
+ elif self.__next_event_type == REPLACE:
226
+ self.__next_event_type = FAILURE
227
+ return self.time_to_replace.random(1).item(), True
@@ -0,0 +1,59 @@
1
+ """Helpers describing the *capabilities* of the reliability models the RBD
2
+ classes consume.
3
+
4
+ Historically the RBD code branched on ``model.dist.name`` string literals
5
+ (e.g. ``"FixedEventProbability"``, ``"Exponential"``) scattered across several
6
+ modules. That couples behaviour tightly to surpyval's internal naming: a
7
+ rename there silently changes results here. Centralising the checks in these
8
+ small helpers keeps that coupling in one place (easy to audit and to cover
9
+ with a compatibility test) and gives the call sites intention-revealing names.
10
+ """
11
+
12
+ from typing import Optional
13
+
14
+ import numpy as np
15
+
16
+ # surpyval distribution names whose event probability does not vary with time,
17
+ # i.e. ``sf(t)`` is constant. These behave as fixed per-demand probabilities
18
+ # rather than lifetime distributions.
19
+ FIXED_PROBABILITY_DIST_NAMES = frozenset(
20
+ {"FixedEventProbability", "Bernoulli"}
21
+ )
22
+
23
+
24
+ def distribution_name(model) -> Optional[str]:
25
+ """Return the surpyval distribution name of ``model``.
26
+
27
+ Returns ``None`` for models that do not expose a surpyval distribution
28
+ (e.g. a ``StandbyModel``, ``RepeatedNode``, nested RBD, or the perfect
29
+ reliability/unreliability helpers).
30
+ """
31
+ dist = getattr(model, "dist", None)
32
+ if dist is None:
33
+ return None
34
+ return getattr(dist, "name", None)
35
+
36
+
37
+ def is_fixed_probability(model) -> bool:
38
+ """True if ``model`` is a time-invariant (fixed) event probability."""
39
+ return distribution_name(model) in FIXED_PROBABILITY_DIST_NAMES
40
+
41
+
42
+ def is_exponential(model) -> bool:
43
+ """True if ``model`` is a surpyval Exponential distribution."""
44
+ return distribution_name(model) == "Exponential"
45
+
46
+
47
+ def model_mean(model) -> float:
48
+ """The model's mean as a plain float.
49
+
50
+ Works around surpyval 0.11's ExactEventTime, whose ``mean()`` raises
51
+ AttributeError (its underlying dist has no ``mean``); for an exact event
52
+ time the mean is simply its parameter.
53
+ """
54
+ try:
55
+ return float(np.atleast_1d(model.mean())[0])
56
+ except AttributeError:
57
+ if distribution_name(model) == "ExactEventTime":
58
+ return float(np.atleast_1d(model.params)[0])
59
+ raise