pvsld 0.7.2__tar.gz → 0.7.3__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 (225) hide show
  1. {pvsld-0.7.2 → pvsld-0.7.3}/CHANGELOG.md +6 -0
  2. {pvsld-0.7.2 → pvsld-0.7.3}/PKG-INFO +1 -1
  3. {pvsld-0.7.2 → pvsld-0.7.3}/README.md +7 -6
  4. {pvsld-0.7.2 → pvsld-0.7.3}/pyproject.toml +1 -1
  5. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/palette.py +11 -0
  6. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/bos.py +36 -10
  7. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/choices.py +23 -0
  8. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/dcbox.py +6 -1
  9. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/engine.py +4 -2
  10. pvsld-0.7.3/tests/test_string_ocpd_poles.py +101 -0
  11. {pvsld-0.7.2 → pvsld-0.7.3}/.gitignore +0 -0
  12. {pvsld-0.7.2 → pvsld-0.7.3}/LICENSE +0 -0
  13. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/.gitkeep +0 -0
  14. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/conductors/.gitkeep +0 -0
  15. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/conductors/viakon-pv-wire-2000v.yaml +0 -0
  16. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/.gitkeep +0 -0
  17. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/growatt-min-tl-x2.yaml +0 -0
  18. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
  19. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/.gitkeep +0 -0
  20. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/etsolar-et-m672bh.yaml +0 -0
  21. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/jinko-jkm-60hl4.yaml +0 -0
  22. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
  23. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/.gitkeep +0 -0
  24. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/clamper-solar.yaml +0 -0
  25. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/connera-shield-dc-4p32a.yaml +0 -0
  26. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/eaton-bussmann-pv-10x38.yaml +0 -0
  27. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/littelfuse-spf.yaml +0 -0
  28. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/schneider-acti9-c60pv-dc.yaml +0 -0
  29. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/squared-qo-2p.yaml +0 -0
  30. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/suntree-siso-40.yaml +0 -0
  31. {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/suntree-sl7n-63.yaml +0 -0
  32. {pvsld-0.7.2 → pvsld-0.7.3}/docs/pypi.md +0 -0
  33. {pvsld-0.7.2 → pvsld-0.7.3}/examples/quick_request.yaml +0 -0
  34. {pvsld-0.7.2 → pvsld-0.7.3}/examples/residential_7p7kwp.yaml +0 -0
  35. {pvsld-0.7.2 → pvsld-0.7.3}/examples/sizing_jinko_growatt.yaml +0 -0
  36. {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/catalogo/SKILL.md +0 -0
  37. {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/diagrama/SKILL.md +0 -0
  38. {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/pro/SKILL.md +0 -0
  39. {pvsld-0.7.2 → pvsld-0.7.3}/sheet_templates/a3_plantilla_v1.dxf +0 -0
  40. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/__init__.py +0 -0
  41. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/__init__.py +0 -0
  42. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/base.py +0 -0
  43. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/dxf.py +0 -0
  44. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/preview.py +0 -0
  45. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/readback.py +0 -0
  46. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/capabilities.py +0 -0
  47. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/__init__.py +0 -0
  48. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/cli.py +0 -0
  49. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/common.py +0 -0
  50. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/conductors.py +0 -0
  51. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/errors.py +0 -0
  52. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/inverters.py +0 -0
  53. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/local.py +0 -0
  54. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/modules.py +0 -0
  55. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/protection.py +0 -0
  56. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/registry.py +0 -0
  57. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/cli.py +0 -0
  58. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/__init__.py +0 -0
  59. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/calc.py +0 -0
  60. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/diagram.py +0 -0
  61. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/draft.py +0 -0
  62. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layers.py +0 -0
  63. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layout.py +0 -0
  64. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layout_sheet.py +0 -0
  65. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/model.py +0 -0
  66. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/policy.py +0 -0
  67. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/rules.py +0 -0
  68. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/severity.py +0 -0
  69. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/sheet.py +0 -0
  70. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/tables.py +0 -0
  71. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/validation.py +0 -0
  72. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/__init__.py +0 -0
  73. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/pipeline.py +0 -0
  74. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/pro.py +0 -0
  75. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/quick_template.yaml +0 -0
  76. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/__init__.py +0 -0
  77. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/console_log.py +0 -0
  78. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/core_console.py +0 -0
  79. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/fake.py +0 -0
  80. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/outputs.py +0 -0
  81. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/graph.py +0 -0
  82. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/__init__.py +0 -0
  83. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/__main__.py +0 -0
  84. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/catalogue_tool.py +0 -0
  85. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/design_tool.py +0 -0
  86. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/handshake.py +0 -0
  87. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/models.py +0 -0
  88. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/preview.py +0 -0
  89. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/prompts.py +0 -0
  90. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/sandbox.py +0 -0
  91. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/server.py +0 -0
  92. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/sizing_tools.py +0 -0
  93. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/py.typed +0 -0
  94. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/__init__.py +0 -0
  95. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/bom.py +0 -0
  96. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/memoria.py +0 -0
  97. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/project_sheet.py +0 -0
  98. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/resources.py +0 -0
  99. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/service.py +0 -0
  100. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/__init__.py +0 -0
  101. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/a3_plantilla_v1.py +0 -0
  102. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/cli.py +0 -0
  103. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/importer.py +0 -0
  104. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/loader.py +0 -0
  105. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/__init__.py +0 -0
  106. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/adapters.py +0 -0
  107. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/io.py +0 -0
  108. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/models.py +0 -0
  109. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/report.py +0 -0
  110. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/strings.py +0 -0
  111. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/specfile.py +0 -0
  112. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/__init__.py +0 -0
  113. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/__init__.py +0 -0
  114. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/build.py +0 -0
  115. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/cli.py +0 -0
  116. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/definitions.py +0 -0
  117. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/dwg.py +0 -0
  118. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/loader.py +0 -0
  119. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/model.py +0 -0
  120. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/validate.py +0 -0
  121. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/__init__.py +0 -0
  122. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/com.py +0 -0
  123. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/fake.py +0 -0
  124. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/pipe.py +0 -0
  125. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/protocol.py +0 -0
  126. {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/streams.py +0 -0
  127. {pvsld-0.7.2 → pvsld-0.7.3}/symbols/pvsld-symbols.dxf +0 -0
  128. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/conftest.py +0 -0
  129. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/s2_support.py +0 -0
  130. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_1_com_baseline.py +0 -0
  131. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_2_plugin_performance.py +0 -0
  132. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_3_plugin_reliability.py +0 -0
  133. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_4_plugin_safety.py +0 -0
  134. {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_5_busy.py +0 -0
  135. {pvsld-0.7.2 → pvsld-0.7.3}/tests/catalogue_helpers.py +0 -0
  136. {pvsld-0.7.2 → pvsld-0.7.3}/tests/conftest.py +0 -0
  137. {pvsld-0.7.2 → pvsld-0.7.3}/tests/finishers/test_core_console_live.py +0 -0
  138. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/inverters/growatt-min-tl-x2.yaml +0 -0
  139. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
  140. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/modules/etsolar-et-m672bh.yaml +0 -0
  141. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
  142. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/protection/schneider-acti9-c60pv-dc.yaml +0 -0
  143. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/protection/suntree-sl7n-63.yaml +0 -0
  144. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/conductors/viakon-pv-wire-2000v.yaml +0 -0
  145. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/growatt-min-tl-x2.yaml +0 -0
  146. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
  147. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/yi-6000.yaml +0 -0
  148. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/etsolar-et-m672bh.yaml +0 -0
  149. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
  150. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/xm-550.yaml +0 -0
  151. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/littelfuse-spf.yaml +0 -0
  152. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/schneider-acti9-c60pv-dc.yaml +0 -0
  153. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/squared-qo-2p.yaml +0 -0
  154. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/suntree-siso-40.yaml +0 -0
  155. {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/suntree-sl7n-63.yaml +0 -0
  156. {pvsld-0.7.2 → pvsld-0.7.3}/tests/golden/residential_7p7kwp.dxf +0 -0
  157. {pvsld-0.7.2 → pvsld-0.7.3}/tests/s1_helpers.py +0 -0
  158. {pvsld-0.7.2 → pvsld-0.7.3}/tests/sizing_helpers.py +0 -0
  159. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_autocad_marker.py +0 -0
  160. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_dxf.py +0 -0
  161. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_dxf_golden.py +0 -0
  162. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_preview.py +0 -0
  163. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_cli.py +0 -0
  164. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_connera.py +0 -0
  165. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_extensions.py +0 -0
  166. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_local.py +0 -0
  167. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_real_records.py +0 -0
  168. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_records.py +0 -0
  169. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_registry.py +0 -0
  170. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_review_gate.py +0 -0
  171. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_spd.py +0 -0
  172. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_validation.py +0 -0
  173. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_definitions.py +0 -0
  174. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_library.py +0 -0
  175. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_model.py +0 -0
  176. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_choices.py +0 -0
  177. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_claude_plugin.py +0 -0
  178. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cli.py +0 -0
  179. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_conduit_fill.py +0 -0
  180. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_calc.py +0 -0
  181. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_diagram.py +0 -0
  182. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_model.py +0 -0
  183. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_rules.py +0 -0
  184. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_str007.py +0 -0
  185. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_dcbox.py +0 -0
  186. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_dcbox_per_string.py +0 -0
  187. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_design.py +0 -0
  188. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_design_pro.py +0 -0
  189. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_draft.py +0 -0
  190. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_example.py +0 -0
  191. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_cli.py +0 -0
  192. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_console_log.py +0 -0
  193. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_core_console.py +0 -0
  194. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_outputs.py +0 -0
  195. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_graph.py +0 -0
  196. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_import_footprint.py +0 -0
  197. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_layout_sheet.py +0 -0
  198. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_library_dwg.py +0 -0
  199. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_handshake.py +0 -0
  200. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_preview.py +0 -0
  201. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_prompts.py +0 -0
  202. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_sandbox.py +0 -0
  203. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_server.py +0 -0
  204. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_sizing.py +0 -0
  205. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_stdio.py +0 -0
  206. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_package.py +0 -0
  207. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_palette.py +0 -0
  208. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_palette_naming.py +0 -0
  209. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_project_metadata.py +0 -0
  210. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_report.py +0 -0
  211. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_resources.py +0 -0
  212. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_bos.py +0 -0
  213. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_cli.py +0 -0
  214. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_diagram.py +0 -0
  215. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_engine.py +0 -0
  216. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_overrides.py +0 -0
  217. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_ranking.py +0 -0
  218. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_strings.py +0 -0
  219. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_switchgear.py +0 -0
  220. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_symbols.py +0 -0
  221. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_com_baseline.py +0 -0
  222. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_client.py +0 -0
  223. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_discovery.py +0 -0
  224. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_protocol.py +0 -0
  225. {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_windows.py +0 -0
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.3] - 2026-11-06
11
+
12
+ ### Changed
13
+
14
+ - The poles of each string's gPV fuse and DC breaker follow `grounding.dc_system`: 2 on an ungrounded array (NOM 690-35), 1 on a grounded one. `pvsld.sizing.bos.string_ocpd_poles` is the rule; sizing, `dc_box`, the DC breaker's voltage and `evaluate_choices` use it, and the palette publishes `poles_by_dc_system`. The box disconnect is unchanged.
15
+
10
16
  ## [0.7.2] - 2026-11-04
11
17
 
12
18
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pvsld
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: PvUnifilar: MCP server and CLI that design and draw Mexican photovoltaic single-line diagrams.
5
5
  Project-URL: Homepage, https://github.com/edu3250/PvUnifilar
6
6
  Project-URL: Issues, https://github.com/edu3250/PvUnifilar/issues
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://github.com/edu3250/PvUnifilar/actions/workflows/ci.yml"><img src="https://github.com/edu3250/PvUnifilar/actions/workflows/ci.yml/badge.svg?branch=develop" alt="CI"></a>
11
- <img src="https://img.shields.io/badge/versi%C3%B3n-v0.7.2-blue" alt="Versión v0.7.2">
11
+ <img src="https://img.shields.io/badge/versi%C3%B3n-v0.7.3-blue" alt="Versión v0.7.3">
12
12
  <img src="https://img.shields.io/badge/python-3.11%20%7C%203.12-blue" alt="Python 3.11 | 3.12">
13
13
  <img src="https://img.shields.io/badge/MCP-servidor%20stdio-8A2BE2" alt="Servidor MCP stdio">
14
14
  <img src="https://img.shields.io/badge/Claude%20Code-plugin-D97757" alt="Plugin de Claude Code">
@@ -162,7 +162,7 @@ Lado CA:
162
162
  **3. Primer arranque.** La primera vez, uv descarga unos 60 MB de dependencias (de 15 segundos a un minuto), y eso puede tardar más que los 30 segundos que Claude Code espera a un servidor. Si `/mcp` muestra `pvsld` como fallido, reconéctalo ahí cuando termine la descarga, o descárgalo antes desde una terminal:
163
163
 
164
164
  ```bash
165
- uvx --from pvsld==0.7.2 pvsld-mcp --version
165
+ uvx --from pvsld==0.7.3 pvsld-mcp --version
166
166
  ```
167
167
 
168
168
  **4. Usarlo.** Pídelo con tus palabras, o usa los comandos:
@@ -208,7 +208,7 @@ El plugin es una capa delgada sobre un **servidor MCP stdio** estándar, `pvsld-
208
208
  <summary><b>Claude Code</b>, sin el plugin</summary>
209
209
 
210
210
  ```bash
211
- claude mcp add pvsld -- uvx --from pvsld==0.7.2 pvsld-mcp
211
+ claude mcp add pvsld -- uvx --from pvsld==0.7.3 pvsld-mcp
212
212
  ```
213
213
 
214
214
  o en el `.mcp.json` de un proyecto:
@@ -218,7 +218,7 @@ o en el `.mcp.json` de un proyecto:
218
218
  "mcpServers": {
219
219
  "pvsld": {
220
220
  "command": "uvx",
221
- "args": ["--from", "pvsld==0.7.2", "pvsld-mcp"],
221
+ "args": ["--from", "pvsld==0.7.3", "pvsld-mcp"],
222
222
  "env": { "PVSLD_OUTPUT_DIR": "out" }
223
223
  }
224
224
  }
@@ -237,7 +237,7 @@ Agrega el mismo servidor a `claude_desktop_config.json` (Settings → Developer
237
237
  "mcpServers": {
238
238
  "pvsld": {
239
239
  "command": "uvx",
240
- "args": ["--from", "pvsld==0.7.2", "pvsld-mcp"],
240
+ "args": ["--from", "pvsld==0.7.3", "pvsld-mcp"],
241
241
  "env": { "PVSLD_OUTPUT_DIR": "C:\\Users\\tu_usuario\\Documents\\unifilares" }
242
242
  }
243
243
  }
@@ -256,7 +256,7 @@ En `.vscode/mcp.json` la clave principal es `servers`:
256
256
  "servers": {
257
257
  "pvsld": {
258
258
  "command": "uvx",
259
- "args": ["--from", "pvsld==0.7.2", "pvsld-mcp"]
259
+ "args": ["--from", "pvsld==0.7.3", "pvsld-mcp"]
260
260
  }
261
261
  }
262
262
  }
@@ -379,6 +379,7 @@ pvsld design examples/quick_request.yaml -o out/demo --no-autocad
379
379
 
380
380
  | Versión | Contenido |
381
381
  |---|---|
382
+ | `v0.7.3` | Polos del fusible y del ITM de cada cadena según el aterrizamiento del arreglo (2 no aterrizado, 1 aterrizado) |
382
383
  | `v0.7.2` | Caja de protecciones CD por cadena (seccionador y DPS propios por cadena); revisión de cada DPS; el dimensionamiento elige el DPS del catálogo |
383
384
  | `v0.7.1` | Nombres de los equipos y valores derivados de las conexiones en la paleta; cables de cada circuito y tubería dibujada en el editor |
384
385
  | `v0.7.0` | Motor para el editor PvUnifilar: paleta de símbolos, revisión de borradores, valores fijados a mano, caja de protecciones CD armada, del dibujo a la especificación y matriz de capacidades; DPS CLAMPER Solar y seccionadores Connera en el catálogo |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pvsld"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "PvUnifilar: MCP server and CLI that design and draw Mexican photovoltaic single-line diagrams."
9
9
  # What PyPI shows. The repository README has relative image paths, which do not work there.
10
10
  readme = "docs/pypi.md"
@@ -34,6 +34,7 @@ from pydantic import BaseModel, ConfigDict, Field
34
34
  from pvsld.catalogue.registry import Component, ComponentRegistry
35
35
  from pvsld.core import model as spec_model
36
36
  from pvsld.resources import data_path
37
+ from pvsld.sizing.bos import STRING_OCPD_POLES_BY_DC_SYSTEM
37
38
  from pvsld.symbols.cfe import get_symbol
38
39
  from pvsld.symbols.cfe.model import (
39
40
  LIBRARY_VERSION,
@@ -277,6 +278,11 @@ class TypeDescriptor(BaseModel):
277
278
  description="Values of the spec section that come from the connections "
278
279
  "(pvsld.graph.graph_to_spec); an editor does not ask for them",
279
280
  )
281
+ poles_by_dc_system: dict[str, int] | None = Field(
282
+ default=None,
283
+ description="Poles of the part by grounding.dc_system (string fuses and DC breakers: "
284
+ "pvsld.sizing.bos.string_ocpd_poles)",
285
+ )
280
286
  symbol: SymbolModel
281
287
  fingerprint: str = Field(description="SHA-256 of this descriptor without the fingerprint")
282
288
 
@@ -344,6 +350,7 @@ class _TypeDef:
344
350
  id_prefix: str | None = None,
345
351
  naming: tuple[NamingRule, ...] = (),
346
352
  derived_fields: tuple[str, ...] = (),
353
+ poles_by_dc_system: dict[str, int] | None = None,
347
354
  ) -> None:
348
355
  self.type_id = type_id
349
356
  self.label_es = label_es
@@ -354,6 +361,7 @@ class _TypeDef:
354
361
  self.id_prefix = id_prefix
355
362
  self.naming = naming
356
363
  self.derived_fields = derived_fields
364
+ self.poles_by_dc_system = poles_by_dc_system
357
365
 
358
366
 
359
367
  def _rule(pattern: str, description_es: str, context: str = "default") -> NamingRule:
@@ -392,6 +400,7 @@ TYPES: tuple[_TypeDef, ...] = (
392
400
  id_prefix="FUS-",
393
401
  naming=(_rule("FUS-{string}", f"FUS-S1: {_PER_STRING}"),),
394
402
  derived_fields=("integrated_in",),
403
+ poles_by_dc_system=dict(STRING_OCPD_POLES_BY_DC_SYSTEM),
395
404
  ),
396
405
  _TypeDef(
397
406
  "dc_breaker",
@@ -403,6 +412,7 @@ TYPES: tuple[_TypeDef, ...] = (
403
412
  id_prefix="DCB-",
404
413
  naming=(_rule("DCB-{string}", f"DCB-S1: {_PER_STRING}"),),
405
414
  derived_fields=("integrated_in",),
415
+ poles_by_dc_system=dict(STRING_OCPD_POLES_BY_DC_SYSTEM),
406
416
  ),
407
417
  _TypeDef(
408
418
  "dc_switch",
@@ -664,6 +674,7 @@ def _type_descriptor(definition: _TypeDef) -> TypeDescriptor:
664
674
  "parameters_schema": schema,
665
675
  "naming": definition.naming,
666
676
  "derived_fields": definition.derived_fields,
677
+ "poles_by_dc_system": definition.poles_by_dc_system,
667
678
  "symbol": _symbol_model(get_symbol(definition.block), add_anchors=True),
668
679
  }
669
680
  draft = TypeDescriptor.model_construct(**fields, fingerprint="")
@@ -65,6 +65,17 @@ STRING_BREAKER_POLES = 2
65
65
  """A string is a two-wire circuit: both conductors are opened (ungrounded array, NOM 690-35)."""
66
66
  STRING_FUSE_POLES = 2
67
67
  """One gPV fuse per conductor of the string, in a two-pole fuse-disconnector."""
68
+ STRING_OCPD_POLES_BY_DC_SYSTEM = {"ungrounded_690_35": 2, "grounded": 1}
69
+ """Poles of a string's fuse or DC breaker by ``grounding.dc_system`` (owner request
70
+ 2026-11-06): both conductors of an ungrounded array (NOM 690-35), only the ungrounded conductor
71
+ of a grounded one (the grounded conductor carries no overcurrent device)."""
72
+
73
+
74
+ def string_ocpd_poles(dc_system: str | None) -> int:
75
+ """Poles of the gPV fuse or DC breaker of each string for ``dc_system`` (default: 2)."""
76
+ return STRING_OCPD_POLES_BY_DC_SYSTEM.get(dc_system or "", STRING_FUSE_POLES)
77
+
78
+
68
79
  LARGEST_SIZE = "4/0 AWG"
69
80
 
70
81
  TODO_STAGE_3_4B = (
@@ -121,10 +132,17 @@ class StringNeed:
121
132
  """Voc of the string at the lowest temperature: the least rated voltage."""
122
133
  fault_a: float
123
134
  """Current the protection may have to interrupt: every string of the input."""
135
+ poles: int = STRING_FUSE_POLES
136
+ """Poles of the protection: :func:`string_ocpd_poles` of the array's ``dc_system``."""
124
137
 
125
138
 
126
139
  def string_need(
127
- *, max_series_fuse_a: float, isc_a: float, strings_per_input: int, voc_cold_string_v: float
140
+ *,
141
+ max_series_fuse_a: float,
142
+ isc_a: float,
143
+ strings_per_input: int,
144
+ voc_cold_string_v: float,
145
+ poles: int = STRING_FUSE_POLES,
128
146
  ) -> StringNeed:
129
147
  """The :class:`StringNeed` of a string of ``isc_a`` with ``strings_per_input`` on its input."""
130
148
  return StringNeed(
@@ -132,6 +150,7 @@ def string_need(
132
150
  maximum_a=max_series_fuse_a,
133
151
  voc_cold_v=voc_cold_string_v,
134
152
  fault_a=strings_per_input * isc_a,
153
+ poles=poles,
135
154
  )
136
155
 
137
156
 
@@ -156,21 +175,21 @@ def fuse_problem(fuse: DcFuse, need: StringNeed) -> str | None:
156
175
 
157
176
 
158
177
  def breaker_fit(breaker: DcBreaker, need: StringNeed) -> tuple[str | None, float, float]:
159
- """``(why, ue_v, icu_ka)`` of a two-pole catalogue ``breaker`` on the string.
178
+ """``(why, ue_v, icu_ka)`` of a catalogue ``breaker`` with ``need.poles`` poles on the string.
160
179
 
161
180
  ``why`` is ``None`` when the breaker can protect it; ``ue_v`` is then the smallest rated
162
181
  voltage at or above Voc(T_min) and ``icu_ka`` its breaking capacity there.
163
182
  """
164
183
  if (why := string_rating_problem(breaker.rated_current_a, need)) is not None:
165
184
  return why, 0.0, 0.0
166
- voltages = [v for v in breaker.ue_options_v(STRING_BREAKER_POLES) if v >= need.voc_cold_v]
185
+ voltages = [v for v in breaker.ue_options_v(need.poles) if v >= need.voc_cold_v]
167
186
  if not voltages:
168
187
  return (
169
- f"sin tensión nominal >= {_n(need.voc_cold_v)} V en {STRING_BREAKER_POLES}P",
188
+ f"sin tensión nominal >= {_n(need.voc_cold_v)} V en {need.poles}P",
170
189
  0.0,
171
190
  0.0,
172
191
  )
173
- icu_ka = breaker.breaking_capacity_ka(STRING_BREAKER_POLES, need.voc_cold_v)
192
+ icu_ka = breaker.breaking_capacity_ka(need.poles, need.voc_cold_v)
174
193
  if icu_ka is None or icu_ka * 1000 < need.fault_a:
175
194
  shown = "sin dato" if icu_ka is None else f"{_g(icu_ka)} kA"
176
195
  return f"capacidad de ruptura {shown} < {_n(need.fault_a)} A de falla", 0.0, 0.0
@@ -178,13 +197,18 @@ def breaker_fit(breaker: DcBreaker, need: StringNeed) -> tuple[str | None, float
178
197
 
179
198
 
180
199
  def _need_of(
181
- module: CatalogueModule, isc_a: float, strings_per_input: int, voc_cold_string_v: float
200
+ module: CatalogueModule,
201
+ isc_a: float,
202
+ strings_per_input: int,
203
+ voc_cold_string_v: float,
204
+ poles: int = STRING_FUSE_POLES,
182
205
  ) -> StringNeed:
183
206
  return string_need(
184
207
  max_series_fuse_a=module.max_series_fuse_a,
185
208
  isc_a=isc_a,
186
209
  strings_per_input=strings_per_input,
187
210
  voc_cold_string_v=voc_cold_string_v,
211
+ poles=poles,
188
212
  )
189
213
 
190
214
 
@@ -225,7 +249,7 @@ def breaker_for(need: StringNeed, breakers: Sequence[DcBreaker], reason_es: str)
225
249
  device_id=name,
226
250
  rating_a=rating,
227
251
  ue_v=ue_v,
228
- poles=STRING_BREAKER_POLES,
252
+ poles=need.poles,
229
253
  icu_ka=icu_ka,
230
254
  minimum_rating_a=need.minimum_a,
231
255
  rejected=tuple(rejected),
@@ -264,7 +288,7 @@ def fuse_for(need: StringNeed, fuses: Sequence[DcFuse], reason_es: str) -> DcOcp
264
288
  device="fuse",
265
289
  rating_a=rating,
266
290
  ue_v=fuse.rated_voltage_v,
267
- poles=STRING_FUSE_POLES,
291
+ poles=need.poles,
268
292
  icu_ka=fuse.breaking_capacity_ka,
269
293
  minimum_rating_a=need.minimum_a,
270
294
  rejected=tuple(rejected),
@@ -280,6 +304,7 @@ def select_string_breaker(
280
304
  breakers: Sequence[DcBreaker],
281
305
  required: bool,
282
306
  reason_es: str,
307
+ poles: int = STRING_BREAKER_POLES,
283
308
  ) -> DcOcpdChoice:
284
309
  """Choose the smallest catalogue breaker that satisfies the criteria of the module docstring.
285
310
 
@@ -288,7 +313,7 @@ def select_string_breaker(
288
313
  """
289
314
  if not required:
290
315
  return DcOcpdChoice(required=False, reason_es=reason_es)
291
- need = _need_of(module, isc_a, strings_per_input, voc_cold_string_v)
316
+ need = _need_of(module, isc_a, strings_per_input, voc_cold_string_v, poles)
292
317
  return breaker_for(need, breakers, reason_es)
293
318
 
294
319
 
@@ -301,6 +326,7 @@ def select_string_fuse(
301
326
  fuses: Sequence[DcFuse],
302
327
  required: bool,
303
328
  reason_es: str,
329
+ poles: int = STRING_FUSE_POLES,
304
330
  ) -> DcOcpdChoice:
305
331
  """Choose the smallest catalogue gPV fuse that satisfies the criteria of the module docstring.
306
332
 
@@ -309,7 +335,7 @@ def select_string_fuse(
309
335
  """
310
336
  if not required:
311
337
  return DcOcpdChoice(required=False, reason_es=reason_es, device="fuse")
312
- need = _need_of(module, isc_a, strings_per_input, voc_cold_string_v)
338
+ need = _need_of(module, isc_a, strings_per_input, voc_cold_string_v, poles)
313
339
  return fuse_for(need, fuses, reason_es)
314
340
 
315
341
 
@@ -294,6 +294,8 @@ class _Evaluator:
294
294
  def string_protections(self) -> None:
295
295
  strings = {s.string_id: s for s in self.derived.strings}
296
296
  modules = {m.id: m for m in self.spec.modules}
297
+ dc_system = self.spec.grounding.dc_system
298
+ poles_needed = bos.string_ocpd_poles(dc_system)
297
299
  for device in self.spec.dc_bos.disconnects:
298
300
  if device.id.startswith(STRING_FUSE_PREFIX):
299
301
  prefix, kind, label = STRING_FUSE_PREFIX, DcFuse, "fusible gPV"
@@ -318,6 +320,7 @@ class _Evaluator:
318
320
  isc_a=string.isc_a,
319
321
  strings_per_input=string.n_parallel,
320
322
  voc_cold_string_v=string.voc_max_string_v,
323
+ poles=poles_needed,
321
324
  )
322
325
  reason = f"Protección de la cadena {string.string_id}."
323
326
  choice = (
@@ -382,6 +385,26 @@ class _Evaluator:
382
385
  proposed=choice.ue_v,
383
386
  why_es=why,
384
387
  )
388
+ # Poles follow grounding.dc_system (owner request 2026-11-06): fewer than needed
389
+ # leaves an ungrounded conductor unprotected; more is proposed down as an update.
390
+ system_es = "aterrizado" if dc_system == "grounded" else "no aterrizado (NOM 690-35)"
391
+ self.record(
392
+ device.id,
393
+ "poles",
394
+ device.poles,
395
+ rule_id="OCP-002",
396
+ problem=None
397
+ if device.poles >= poles_needed
398
+ else (
399
+ "OCP-002",
400
+ Severity.ERROR,
401
+ f"{device.id}: {device.poles} polo(s); el arreglo {system_es} necesita "
402
+ f"{poles_needed} por cadena.",
403
+ ),
404
+ ok_es=f"{device.id}: {device.poles} polo(s) para un arreglo {system_es}.",
405
+ proposed=poles_needed,
406
+ why_es=f"Arreglo {system_es}: {poles_needed} polo(s) por cadena.",
407
+ )
385
408
 
386
409
  def _string_model_problem(
387
410
  self, model: str, kind: type[Any], need: bos.StringNeed, ie_a: float
@@ -295,6 +295,10 @@ def dc_box(
295
295
  except (ValidationError, KeyError, TypeError): # pragma: no cover - the draft checked them
296
296
  return DcBox(draft, skipped_es="No se armó la caja: los módulos no son válidos.")
297
297
  lock = _Locks(locks, _drawn(data))
298
+ grounding = data.get("grounding") if isinstance(data, Mapping) else None
299
+ ocpd_poles = bos.string_ocpd_poles(
300
+ grounding.get("dc_system") if isinstance(grounding, Mapping) else None
301
+ )
298
302
  if layout == "auto":
299
303
  layout = box_layout(data) or "shared"
300
304
  inverter = get_symbol("PVSLD_INV")
@@ -318,6 +322,7 @@ def dc_box(
318
322
  isc_a=string.isc_a,
319
323
  strings_per_input=string.n_parallel,
320
324
  voc_cold_string_v=string.voc_max_string_v,
325
+ poles=ocpd_poles,
321
326
  )
322
327
  required, why = bos.string_ocpd_required(module, string.isc_a, string.n_parallel)
323
328
  reason = why if required else f"{why} {DESIGN_CHOICE_ES}"
@@ -331,7 +336,7 @@ def dc_box(
331
336
  if choice.rating_a is not None:
332
337
  values = {"ie_a": choice.rating_a, "ue_v": choice.ue_v, "model": choice.device_id}
333
338
  values, kept = lock.keep(part_id, values)
334
- poles = choice.poles or (bos.STRING_FUSE_POLES if is_fuse else bos.STRING_BREAKER_POLES)
339
+ poles = choice.poles or ocpd_poles
335
340
  connector, entry, exit_ = f"CX-{sid}", f"XE-{sid}", f"XS-{sid}"
336
341
  children += [
337
342
  BoxChild(
@@ -231,6 +231,8 @@ def _size_bos(
231
231
  "strings_per_input": metrics.strings_per_mppt,
232
232
  "voc_cold_string_v": metrics.voc_cold_string_v,
233
233
  "required": needed,
234
+ # 1 pole on a grounded array, 2 on an ungrounded one (owner request 2026-11-06)
235
+ "poles": bos.string_ocpd_poles((request.template.get("grounding") or {}).get("dc_system")),
234
236
  }
235
237
  dc_ocpd = None
236
238
  if request.dc_ocpd_device == "fuse":
@@ -512,10 +514,10 @@ def _build_spec(
512
514
  {
513
515
  "id": BOX_SWITCH_ID,
514
516
  "integrated_in": None,
515
- "poles": dc_ocpd.poles * config.n_strings,
517
+ "poles": bos.STRING_SWITCH_POLES * config.n_strings,
516
518
  **(
517
519
  {
518
- "poles": switch.poles or dc_ocpd.poles * config.n_strings,
520
+ "poles": switch.poles or bos.STRING_SWITCH_POLES * config.n_strings,
519
521
  "ue_v": switch.voltage_v,
520
522
  "ie_a": switch.rating_a,
521
523
  "model": switch.device_id,
@@ -0,0 +1,101 @@
1
+ """String fuse and DC breaker poles follow grounding.dc_system (owner request 2026-11-06)."""
2
+
3
+ import copy
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import pytest
8
+
9
+ from pvsld.catalogue import ComponentRegistry
10
+ from pvsld.palette import build_palette
11
+ from pvsld.resources import data_path
12
+ from pvsld.sizing import bos, size_pv_system
13
+ from pvsld.sizing.choices import evaluate_choices
14
+ from pvsld.sizing.dcbox import dc_box
15
+ from pvsld.sizing.io import load_request
16
+
17
+
18
+ @pytest.fixture(scope="module")
19
+ def registry() -> ComponentRegistry:
20
+ return ComponentRegistry.load(data_path("datasheets/records"))
21
+
22
+
23
+ @pytest.fixture(scope="module")
24
+ def request_path() -> Path:
25
+ return Path(data_path("examples/sizing_jinko_growatt.yaml"))
26
+
27
+
28
+ def _sized(registry, request_path, dc_system: str) -> dict[str, Any]:
29
+ request = load_request(request_path)
30
+ template = copy.deepcopy(request.template)
31
+ template["grounding"]["dc_system"] = dc_system
32
+ request = request.model_copy(update={"template": template})
33
+ result = size_pv_system(request, registry)
34
+ assert result.candidates, [r.issues for r in result.rejected][:3]
35
+ return result.candidates[0].spec
36
+
37
+
38
+ def _fuse(spec: dict[str, Any]) -> dict[str, Any]:
39
+ return next(d for d in spec["dc_bos"]["disconnects"] if d["id"] == "FUS-S1")
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ ("dc_system", "poles"), [("ungrounded_690_35", 2), ("grounded", 1), (None, 2)]
44
+ )
45
+ def test_the_rule(dc_system, poles) -> None:
46
+ assert bos.string_ocpd_poles(dc_system) == poles
47
+
48
+
49
+ def test_sizing_follows_the_dc_system(registry, request_path) -> None:
50
+ assert _fuse(_sized(registry, request_path, "ungrounded_690_35"))["poles"] == 2
51
+ grounded = _sized(registry, request_path, "grounded")
52
+ assert _fuse(grounded)["poles"] == 1
53
+ # the box disconnect is unchanged (owner 2026-11-06: reviewed separately)
54
+ switch = next(d for d in grounded["dc_bos"]["disconnects"] if d["id"] == "DCD-CD1")
55
+ assert switch["poles"] == 4
56
+
57
+
58
+ def test_dc_box_follows_the_dc_system(registry, request_path) -> None:
59
+ spec = _sized(registry, request_path, "ungrounded_690_35")
60
+ spec["grounding"]["dc_system"] = "grounded"
61
+ fuse = dc_box(spec, registry=registry).child("FUS-S1")
62
+ assert fuse.poles == 1
63
+
64
+
65
+ def test_breaker_voltage_uses_the_same_poles(registry) -> None:
66
+ need = bos.string_need(
67
+ max_series_fuse_a=35, isc_a=18.17, strings_per_input=1, voc_cold_string_v=269, poles=1
68
+ )
69
+ breaker = registry.get("SUNTREE-SL7N-63-32A")
70
+ why, _ue, _icu = bos.breaker_fit(breaker, need)
71
+ assert why is not None
72
+ assert "1P" in why # the 1-pole SL7N stops at 250 V
73
+ two = bos.string_need(
74
+ max_series_fuse_a=35, isc_a=18.17, strings_per_input=1, voc_cold_string_v=269, poles=2
75
+ )
76
+ assert bos.breaker_fit(breaker, two)[0] is None
77
+
78
+
79
+ def test_choices_propose_the_poles(registry, request_path) -> None:
80
+ spec = _sized(registry, request_path, "ungrounded_690_35")
81
+ spec["grounding"]["dc_system"] = "grounded"
82
+ report = evaluate_choices(spec, registry=registry)
83
+ check = next(c for c in report.checks if c.target == "FUS-S1.poles")
84
+ assert check.ok # 2 poles still protect the ungrounded conductor
85
+ assert {(u.target, u.proposed) for u in report.updates} >= {("FUS-S1.poles", 1)}
86
+ spec["grounding"]["dc_system"] = "ungrounded_690_35"
87
+ _fuse(spec)["poles"] = 1
88
+ check = next(
89
+ c for c in evaluate_choices(spec, registry=registry).checks if c.target == "FUS-S1.poles"
90
+ )
91
+ assert (check.ok, check.rule_id) == (False, "OCP-002")
92
+
93
+
94
+ def test_palette_publishes_the_rule() -> None:
95
+ palette = build_palette()
96
+ for type_id in ("dc_fuse_holder", "dc_breaker"):
97
+ assert palette.type(type_id).poles_by_dc_system == {
98
+ "ungrounded_690_35": 2,
99
+ "grounded": 1,
100
+ }
101
+ assert palette.type("dc_switch").poles_by_dc_system is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes