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.
- {pvsld-0.7.2 → pvsld-0.7.3}/CHANGELOG.md +6 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/PKG-INFO +1 -1
- {pvsld-0.7.2 → pvsld-0.7.3}/README.md +7 -6
- {pvsld-0.7.2 → pvsld-0.7.3}/pyproject.toml +1 -1
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/palette.py +11 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/bos.py +36 -10
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/choices.py +23 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/dcbox.py +6 -1
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/engine.py +4 -2
- pvsld-0.7.3/tests/test_string_ocpd_poles.py +101 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/.gitignore +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/LICENSE +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/.gitkeep +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/conductors/.gitkeep +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/conductors/viakon-pv-wire-2000v.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/.gitkeep +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/growatt-min-tl-x2.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/.gitkeep +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/etsolar-et-m672bh.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/jinko-jkm-60hl4.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/.gitkeep +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/clamper-solar.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/connera-shield-dc-4p32a.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/eaton-bussmann-pv-10x38.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/littelfuse-spf.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/schneider-acti9-c60pv-dc.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/squared-qo-2p.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/suntree-siso-40.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/datasheets/records/protection/suntree-sl7n-63.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/docs/pypi.md +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/examples/quick_request.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/examples/residential_7p7kwp.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/examples/sizing_jinko_growatt.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/catalogo/SKILL.md +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/diagrama/SKILL.md +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/plugins/pv-unifilar/skills/pro/SKILL.md +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/sheet_templates/a3_plantilla_v1.dxf +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/base.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/dxf.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/preview.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/backends/readback.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/capabilities.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/common.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/conductors.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/errors.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/inverters.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/local.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/modules.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/protection.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/catalogue/registry.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/calc.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/diagram.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/draft.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layers.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layout.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/layout_sheet.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/model.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/policy.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/rules.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/severity.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/sheet.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/tables.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/core/validation.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/pipeline.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/pro.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/design/quick_template.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/console_log.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/core_console.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/fake.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/finishers/outputs.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/graph.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/__main__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/catalogue_tool.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/design_tool.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/handshake.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/models.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/preview.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/prompts.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/sandbox.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/server.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/mcp/sizing_tools.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/py.typed +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/bom.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/memoria.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/report/project_sheet.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/resources.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/service.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/a3_plantilla_v1.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/importer.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sheets/loader.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/adapters.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/io.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/models.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/report.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/sizing/strings.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/specfile.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/build.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/definitions.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/dwg.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/loader.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/model.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/symbols/cfe/validate.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/__init__.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/com.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/fake.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/pipe.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/protocol.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/src/pvsld/transports/streams.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/symbols/pvsld-symbols.dxf +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/conftest.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/s2_support.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_1_com_baseline.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_2_plugin_performance.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_3_plugin_reliability.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_4_plugin_safety.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/autocad/test_s2_5_busy.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/catalogue_helpers.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/conftest.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/finishers/test_core_console_live.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/inverters/growatt-min-tl-x2.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/modules/etsolar-et-m672bh.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/protection/schneider-acti9-c60pv-dc.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/catalogue/protection/suntree-sl7n-63.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/conductors/viakon-pv-wire-2000v.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/growatt-min-tl-x2.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/huawei-sun2000-ktl-l1.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/inverters/yi-6000.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/etsolar-et-m672bh.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/jinko-jkm-66hl4m-bdv.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/modules/xm-550.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/littelfuse-spf.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/schneider-acti9-c60pv-dc.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/squared-qo-2p.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/suntree-siso-40.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/fixtures/sizing/records/protection/suntree-sl7n-63.yaml +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/golden/residential_7p7kwp.dxf +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/s1_helpers.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/sizing_helpers.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_autocad_marker.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_dxf.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_dxf_golden.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_backend_preview.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_connera.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_extensions.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_local.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_real_records.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_records.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_registry.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_review_gate.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_spd.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_catalogue_validation.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_definitions.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_library.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cfe_model.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_choices.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_claude_plugin.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_conduit_fill.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_calc.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_diagram.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_model.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_rules.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_core_str007.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_dcbox.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_dcbox_per_string.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_design.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_design_pro.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_draft.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_example.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_console_log.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_core_console.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_finisher_outputs.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_graph.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_import_footprint.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_layout_sheet.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_library_dwg.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_handshake.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_preview.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_prompts.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_sandbox.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_server.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_sizing.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_mcp_stdio.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_package.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_palette.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_palette_naming.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_project_metadata.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_report.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_resources.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_bos.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_cli.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_diagram.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_engine.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_overrides.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_ranking.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_strings.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_sizing_switchgear.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_symbols.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_com_baseline.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_client.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_discovery.py +0 -0
- {pvsld-0.7.2 → pvsld-0.7.3}/tests/test_transport_pipe_protocol.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*,
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
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,
|
|
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=
|
|
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=
|
|
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
|
|
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":
|
|
517
|
+
"poles": bos.STRING_SWITCH_POLES * config.n_strings,
|
|
516
518
|
**(
|
|
517
519
|
{
|
|
518
|
-
"poles": switch.poles or
|
|
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
|
|
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
|
|
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
|