nwp500-python 9.7.0__tar.gz → 9.8.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.
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/CHANGELOG.rst +38 -0
- {nwp500_python-9.7.0/src/nwp500_python.egg-info → nwp500_python-9.8.0}/PKG-INFO +1 -1
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/calibrate-simulator.rst +44 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/sim.rst +4 -2
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/calibration.py +205 -111
- nwp500_python-9.8.0/src/nwp500/sim_calibrate.py +1202 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- nwp500_python-9.8.0/src/nwp500_python.egg-info/scm_version.json +8 -0
- nwp500_python-9.8.0/tests/test_sim_calibrate.py +1380 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_change_history.py +45 -0
- nwp500_python-9.7.0/src/nwp500/sim_calibrate.py +0 -517
- nwp500_python-9.7.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- nwp500_python-9.7.0/tests/test_sim_calibrate.py +0 -608
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.coveragerc +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.gitignore +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/LICENSE.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/Makefile +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/README.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/RELEASE.md +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/Makefile +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/conf.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/draws.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/heating-elements.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/recovery-speed.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/simulator.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/standby-loss.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/tank-energy.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/tou-recovery-cap.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/unknown-values.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/explanation/what-starts-a-recovery.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/bluetooth.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/simulate-heater.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/index.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/events.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/reference/simulator-inputs.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/README.md +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/mask.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/lower-setpoint.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/plan.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/simulator/scenario.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/pyproject.toml +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/README.md +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/bump_version.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/format.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/lint.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/setup.cfg +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/setup.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/__main__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/client.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/mqtt_adapter.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/ble/protocol.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/ble_backend.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/calibration.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/output_formatters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/presentation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/recording.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/rich_output.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/simulate.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/events.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/account.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/diagnostics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/feature.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/firmware.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/maintenance.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/status.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/models/tou.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/_control_commands.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/_device_subscriptions.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/client.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/command_queue.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/connection.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/state_tracker.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/subscriptions.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/types.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt/utils.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/mqtt_events.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/api.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/attribution.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/book.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/circulation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/codec.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/cold_supply.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/controller.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/decay.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/defaults.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/delivery.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/draw_forecast.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/engine.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/extensions.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/heat_pump_recovery.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/heating_trajectory.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/inputs.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/location_air.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/meter.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/observed.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/params.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/__init__.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/compressor.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/draw.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/element.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/refrigerant.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/physics/tank.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/profile.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/ready.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/record.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/result.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/schedules.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/tank_physics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/tou_triggers.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim/units.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_adapters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_home_assistant.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/sim_live.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/status_history.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/tou_schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/SOURCES.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/scm_file_list.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/_sim_site.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/conftest.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/engine_golden.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/heater_params.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/fixtures/sim/heater_params_fitted.json +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_api_endpoints_201.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_app_queries.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_auth_session_refresh.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_ble_mqtt_adapter.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_ble_protocol.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_canonical_schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_calibrate.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_simulate.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_cli_tristate_presentation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_command_queue.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_device_rest_models.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_events.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_models.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_control_fail.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_events.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_last_will.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_mqtt_reliability.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_navilink_201_enums.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_protocol_correctness.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_reservation_validation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_adapters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_api.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_book.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_book_history.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_calibration.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_circulation.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_codec.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_cold_supply.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_controller.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_defaults.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_draw_forecast.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_draw_loss.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_element.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_element_fit.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_forecast.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_golden.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_home_assistant.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_hot_water.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_import_contract.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_live.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_location_air.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_meter.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_no_clock_no_state.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_observed.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_profile.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_readiness.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_readiness_fit.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_ready.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_ready_api.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_replay.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_schedules.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_sim_tank_physics.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_status_history.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_tou_schedule.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_unit_system_process_wide.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/tox.ini +0 -0
- {nwp500_python-9.7.0 → nwp500_python-9.8.0}/uv.lock +0 -0
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
Changelog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
Version 9.8.0 (2026-10-21)
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Added
|
|
9
|
+
-----
|
|
10
|
+
- **Refine a fit without making it worse** (``keep_better=True`` on
|
|
11
|
+
``sim_calibrate.calibrate`` and ``calibrate_changes``, #184). Each
|
|
12
|
+
group of terms holds out its latest evidence from its refit (the last
|
|
13
|
+
fifth, once it has 10 items) and keeps the existing values unless the
|
|
14
|
+
refit scores better there, by the error its own fit minimizes. The
|
|
15
|
+
terms in a group are kept or replaced together. ``Calibrated.judged``
|
|
16
|
+
records each judgement, and the report's reasons give both scores.
|
|
17
|
+
``sim_calibrate.score(done, params)`` scores any parameters on that
|
|
18
|
+
held-out evidence.
|
|
19
|
+
- ``sim.calibration.split_recoveries``, ``recovery_minutes``,
|
|
20
|
+
``power_cycles`` and ``cop_cycles``: the evidence the compressor and
|
|
21
|
+
power fits use, and their predictions, which ``fit_compressor``,
|
|
22
|
+
``fit_power_scale`` and ``fit_cop_scale`` now take from.
|
|
23
|
+
|
|
24
|
+
Fixed
|
|
25
|
+
-----
|
|
26
|
+
- **The report names compressor runs without an air temperature.**
|
|
27
|
+
A recovery is timed against the air, so a run with none (no air
|
|
28
|
+
given, and no heater ambient reading while the compressor runs) is
|
|
29
|
+
left out of the recoveries. The report blamed the heater instead:
|
|
30
|
+
"0 recoveries reached their target". ``Calibrated`` now counts the
|
|
31
|
+
runs (``n_runs``, ``n_runs_without_air``), and the compressor term's
|
|
32
|
+
reason and the notes say how many had no air
|
|
33
|
+
(``sim_calibrate.air_unread_note``).
|
|
34
|
+
- ``sim.calibration.with_standby`` quotes the fitted UA at the ΔT the
|
|
35
|
+
law was fitted at (50 °F). It kept the ``ua_ref_delta_t_f`` of the
|
|
36
|
+
parameters it was given, so a refit applied over parameters quoted at
|
|
37
|
+
another ΔT ran as a different loss law.
|
|
38
|
+
- ``sim.calibration.fit_cop_scale`` takes the integrator's COP on the
|
|
39
|
+
heater's own tank (``HeaterParams.tank``), as the simulation does. It
|
|
40
|
+
used the default tank's mass, so a heater with another tank was fitted
|
|
41
|
+
against a COP the simulation does not run.
|
|
42
|
+
|
|
5
43
|
Version 9.7.0 (2026-10-21)
|
|
6
44
|
==========================
|
|
7
45
|
|
|
@@ -223,6 +223,44 @@ Assistant integration, can calibrate the same way:
|
|
|
223
223
|
4. Store ``params_to_document(done.calibration.params)``, and show the
|
|
224
224
|
``report(done)`` rows and ``done.notes``.
|
|
225
225
|
|
|
226
|
+
Refine an existing fit
|
|
227
|
+
----------------------
|
|
228
|
+
|
|
229
|
+
A fit made on months of history should not give way to one made on the
|
|
230
|
+
last ten days unless the new one predicts better. Pass the existing
|
|
231
|
+
fit as ``params`` with ``keep_better=True``:
|
|
232
|
+
|
|
233
|
+
.. code-block:: python
|
|
234
|
+
|
|
235
|
+
done = sim_calibrate.calibrate(
|
|
236
|
+
recorded(days), ambient=65.0, params=existing, keep_better=True
|
|
237
|
+
)
|
|
238
|
+
for j in done.judged:
|
|
239
|
+
print(j.group, j.kept, j.base_error, j.refit_error, j.why)
|
|
240
|
+
|
|
241
|
+
Each group of terms holds out its latest evidence from its refit:
|
|
242
|
+
|
|
243
|
+
- the standby loss and its exponent: its quiet windows;
|
|
244
|
+
- the zone gap time constant: its windows that open a gap;
|
|
245
|
+
- the compressor constant and the evaporator approach: its reached
|
|
246
|
+
recoveries;
|
|
247
|
+
- the power scale: the metered compressor cycles it can use;
|
|
248
|
+
- the COP terms: the cycles the COP fit can use (without a meter of
|
|
249
|
+
draws, those no water was drawn around).
|
|
250
|
+
|
|
251
|
+
Once a group has 10 items, the latest fifth (at least 2) is held out.
|
|
252
|
+
The refit replaces the existing values only where it scores better on
|
|
253
|
+
them, by the error its own fit minimizes. With fewer items the existing
|
|
254
|
+
values stay. The terms in a group are kept or replaced together, and the
|
|
255
|
+
report's reasons say which, with both scores.
|
|
256
|
+
``sim_calibrate.score(done, params)`` gives those errors for any
|
|
257
|
+
parameters.
|
|
258
|
+
|
|
259
|
+
Use it over a fit of this heater. The defaults are another heater's fit,
|
|
260
|
+
and ``keep_better`` keeps them wherever there is too little evidence to
|
|
261
|
+
judge by. A group the existing fit lists as unfitted takes its refit, as
|
|
262
|
+
without ``keep_better``.
|
|
263
|
+
|
|
226
264
|
``sim.calibration.calibrate`` underneath takes the frame
|
|
227
265
|
``sim_adapters.history`` builds; ``sim_adapters.add_inputs`` adds the
|
|
228
266
|
air, the humidity, a meter's draws and the circulation's runs to it.
|
|
@@ -294,6 +332,12 @@ leave out what your installation does not record. ``read_heater_history``:
|
|
|
294
332
|
- notes a value it could not read, and an entity whose history starts
|
|
295
333
|
after ``start``.
|
|
296
334
|
|
|
335
|
+
A recovery is timed against the air, so the compressor and power fits
|
|
336
|
+
need an air temperature over each compressor run: the air beside the
|
|
337
|
+
heater (``record=``, ``air`` or ``ambient``), or the heater's own reading
|
|
338
|
+
(``ambient`` in ``HeaterEntities``). Runs without one are no recovery,
|
|
339
|
+
and the report says how many there were.
|
|
340
|
+
|
|
297
341
|
Give ``anti_legionella`` where it is recorded. Without it an
|
|
298
342
|
anti-legionella cycle is not kept out of the recoveries, and the notes
|
|
299
343
|
say so.
|
|
@@ -150,7 +150,8 @@ Hot water and the heat book
|
|
|
150
150
|
|
|
151
151
|
.. automodule:: nwp500.sim.calibration
|
|
152
152
|
:members: calibrate, Calibration, fit_standby, StandbyFit, StandbyWindow,
|
|
153
|
-
fit_compressor, CompressorFit,
|
|
153
|
+
fit_compressor, CompressorFit, split_recoveries, recovery_minutes,
|
|
154
|
+
clean_draws, CleanDraw, crossing,
|
|
154
155
|
demonstrated, frontier, capacity, held_out, fit_evidence,
|
|
155
156
|
EvidenceFit, cold_lower_cap, RecordedBook, fit_book_terms, BookFit,
|
|
156
157
|
book_error_bands, book_error_btu, empty_point,
|
|
@@ -163,7 +164,7 @@ Hot water and the heat book
|
|
|
163
164
|
element_off_points, element_power, element_grid, element_runs,
|
|
164
165
|
fit_element_rates, ElementRateFit, day_ci,
|
|
165
166
|
energy_saver_off_deficit, upper_start_edge, fit_power,
|
|
166
|
-
PowerFit, fit_power_scale,
|
|
167
|
+
PowerFit, fit_power_scale, power_cycles, cop_cycles, CopCycle,
|
|
167
168
|
fit_cop_scale, solve_cop_fit, solve_cop_scale, draw_free_withdrawn,
|
|
168
169
|
metered_withdrawn, metered_lead,
|
|
169
170
|
path_to_end, with_power, snapshots, recoveries, quiet_windows,
|
|
@@ -299,6 +300,7 @@ Recorded history
|
|
|
299
300
|
|
|
300
301
|
.. automodule:: nwp500.sim_calibrate
|
|
301
302
|
:members: calibrate, calibrate_changes, Calibrated, report, Term, max_gap,
|
|
303
|
+
score, Judged, HeldOut, GROUPS, air_unread_note,
|
|
302
304
|
CalibrationInputError, DEFAULT_HUMIDITY_PCT, GAP_SAMPLES, MIN_GAP,
|
|
303
305
|
DEFAULT_BASE_NOTE, NO_AIR_NOTE
|
|
304
306
|
|
|
@@ -449,6 +449,92 @@ class CompressorFit:
|
|
|
449
449
|
two_zone: bool
|
|
450
450
|
|
|
451
451
|
|
|
452
|
+
_RECOVERY_NEED = (
|
|
453
|
+
"t_start_lower",
|
|
454
|
+
"delta_t_lower",
|
|
455
|
+
"t_amb_mean",
|
|
456
|
+
"duration_minutes",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def split_recoveries(
|
|
461
|
+
recoveries: pd.DataFrame,
|
|
462
|
+
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
463
|
+
"""``(fitted, held_out)``: the recoveries a compressor fit uses, in
|
|
464
|
+
time order where ``start_time`` says, the latest ``HELDOUT_FRACTION``
|
|
465
|
+
(at least ``MIN_HELDOUT``) held out once there are
|
|
466
|
+
``MIN_RECOVERIES_FOR_SPLIT``. Usable: the figures known and a
|
|
467
|
+
duration from ``MIN_RECOVERY_MINUTES`` to ``MAX_RECOVERY_MINUTES``."""
|
|
468
|
+
if any(c not in recoveries.columns for c in _RECOVERY_NEED):
|
|
469
|
+
empty = recoveries.iloc[0:0]
|
|
470
|
+
return empty, empty
|
|
471
|
+
df = recoveries.dropna(subset=list(_RECOVERY_NEED))
|
|
472
|
+
minutes = df["duration_minutes"]
|
|
473
|
+
df = df.loc[
|
|
474
|
+
(minutes >= MIN_RECOVERY_MINUTES) & (minutes <= MAX_RECOVERY_MINUTES)
|
|
475
|
+
].copy()
|
|
476
|
+
if "start_time" in df.columns:
|
|
477
|
+
df = df.sort_values("start_time").reset_index(drop=True)
|
|
478
|
+
n = len(df)
|
|
479
|
+
n_held = (
|
|
480
|
+
max(MIN_HELDOUT, round(n * HELDOUT_FRACTION))
|
|
481
|
+
if n >= MIN_RECOVERIES_FOR_SPLIT
|
|
482
|
+
else 0
|
|
483
|
+
)
|
|
484
|
+
return df.iloc[: n - n_held], df.iloc[n - n_held :]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def recovery_minutes(
|
|
488
|
+
recoveries: pd.DataFrame,
|
|
489
|
+
compressor_constant: float,
|
|
490
|
+
evap_approach_f: float,
|
|
491
|
+
*,
|
|
492
|
+
compressor: CompressorParams,
|
|
493
|
+
loss: LossParams,
|
|
494
|
+
tank: TankParams,
|
|
495
|
+
two_zone: bool,
|
|
496
|
+
gaps: bool,
|
|
497
|
+
) -> np.ndarray:
|
|
498
|
+
"""Each recovery's duration as the physics predicts it, minutes, on
|
|
499
|
+
``fit_compressor``'s columns; ``gaps`` uses its
|
|
500
|
+
``stratification_gap``, else none."""
|
|
501
|
+
rows = len(recoveries)
|
|
502
|
+
start = np.asarray(recoveries["t_start_lower"], dtype=float)
|
|
503
|
+
target = start + np.asarray(recoveries["delta_t_lower"], dtype=float)
|
|
504
|
+
air = np.asarray(recoveries["t_amb_mean"], dtype=float)
|
|
505
|
+
humidity = np.asarray(
|
|
506
|
+
recoveries["h_amb_start"]
|
|
507
|
+
if "h_amb_start" in recoveries.columns
|
|
508
|
+
else np.full(rows, 50.0),
|
|
509
|
+
dtype=float,
|
|
510
|
+
)
|
|
511
|
+
gap = np.asarray(
|
|
512
|
+
recoveries["stratification_gap"] if gaps else np.zeros(rows),
|
|
513
|
+
dtype=float,
|
|
514
|
+
)
|
|
515
|
+
return np.array(
|
|
516
|
+
[
|
|
517
|
+
predict_physics_detailed(
|
|
518
|
+
t_start_lower=float(start[i]),
|
|
519
|
+
hp_lower_off=float(target[i]),
|
|
520
|
+
t_amb_mean=float(air[i]),
|
|
521
|
+
compressor_constant=compressor_constant,
|
|
522
|
+
evap_approach_f=evap_approach_f,
|
|
523
|
+
compressor=compressor,
|
|
524
|
+
loss=loss,
|
|
525
|
+
h_amb_start=float(humidity[i]),
|
|
526
|
+
ua_btu_hr_f=loss.ua_btu_hr_f,
|
|
527
|
+
ua_exponent=loss.ua_exponent,
|
|
528
|
+
ua_ref_delta_t=loss.ua_ref_delta_t_f,
|
|
529
|
+
stratification_gap=float(gap[i]),
|
|
530
|
+
two_zone=two_zone,
|
|
531
|
+
tank=tank,
|
|
532
|
+
).minutes
|
|
533
|
+
for i in range(rows)
|
|
534
|
+
]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
452
538
|
def fit_compressor(
|
|
453
539
|
recoveries: pd.DataFrame,
|
|
454
540
|
compressor: CompressorParams,
|
|
@@ -470,27 +556,13 @@ def fit_compressor(
|
|
|
470
556
|
``stratification_gap`` (upper minus lower at the start; without it
|
|
471
557
|
the fit falls back to the one-zone integrator, so the constant stays
|
|
472
558
|
the one the simulation will use) and ``start_time`` (to hold out the
|
|
473
|
-
latest). None with fewer than
|
|
559
|
+
latest, ``split_recoveries``). None with fewer than
|
|
560
|
+
``MIN_RECOVERIES`` usable.
|
|
474
561
|
"""
|
|
475
|
-
|
|
476
|
-
if
|
|
477
|
-
return None
|
|
478
|
-
df = recoveries.dropna(subset=need)
|
|
479
|
-
minutes = df["duration_minutes"]
|
|
480
|
-
df = df.loc[
|
|
481
|
-
(minutes >= MIN_RECOVERY_MINUTES) & (minutes <= MAX_RECOVERY_MINUTES)
|
|
482
|
-
].copy()
|
|
483
|
-
if len(df) < MIN_RECOVERIES:
|
|
562
|
+
fit_df, held_df = split_recoveries(recoveries)
|
|
563
|
+
if len(fit_df) + len(held_df) < MIN_RECOVERIES:
|
|
484
564
|
return None
|
|
485
|
-
|
|
486
|
-
df = df.sort_values("start_time").reset_index(drop=True)
|
|
487
|
-
n = len(df)
|
|
488
|
-
n_held = (
|
|
489
|
-
max(MIN_HELDOUT, round(n * HELDOUT_FRACTION))
|
|
490
|
-
if n >= MIN_RECOVERIES_FOR_SPLIT
|
|
491
|
-
else 0
|
|
492
|
-
)
|
|
493
|
-
fit_df, held_df = df.iloc[: n - n_held], df.iloc[n - n_held :]
|
|
565
|
+
n_held = len(held_df)
|
|
494
566
|
# Usable gaps are judged on the rows fitted, as the model's fit did:
|
|
495
567
|
# a gap the fit has not got cannot be the mass model's.
|
|
496
568
|
has_gaps = "stratification_gap" in fit_df.columns and bool(
|
|
@@ -498,56 +570,21 @@ def fit_compressor(
|
|
|
498
570
|
)
|
|
499
571
|
two_zone = two_zone and has_gaps
|
|
500
572
|
|
|
501
|
-
def columns(
|
|
502
|
-
frame: pd.DataFrame, gaps: bool = has_gaps
|
|
503
|
-
) -> tuple[np.ndarray, ...]:
|
|
504
|
-
rows = len(frame)
|
|
505
|
-
return (
|
|
506
|
-
np.asarray(frame["t_start_lower"], dtype=float),
|
|
507
|
-
np.asarray(
|
|
508
|
-
frame["t_start_lower"] + frame["delta_t_lower"], dtype=float
|
|
509
|
-
),
|
|
510
|
-
np.asarray(frame["t_amb_mean"], dtype=float),
|
|
511
|
-
np.asarray(
|
|
512
|
-
frame["h_amb_start"]
|
|
513
|
-
if "h_amb_start" in frame.columns
|
|
514
|
-
else np.full(rows, 50.0),
|
|
515
|
-
dtype=float,
|
|
516
|
-
),
|
|
517
|
-
np.asarray(
|
|
518
|
-
frame["stratification_gap"] if gaps else np.zeros(rows),
|
|
519
|
-
dtype=float,
|
|
520
|
-
),
|
|
521
|
-
np.asarray(frame["duration_minutes"], dtype=float),
|
|
522
|
-
)
|
|
523
|
-
|
|
524
573
|
def predicted(
|
|
525
|
-
|
|
574
|
+
frame: pd.DataFrame, k: float, approach: float, gaps: bool = has_gaps
|
|
526
575
|
) -> np.ndarray:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
compressor=compressor,
|
|
537
|
-
loss=loss,
|
|
538
|
-
h_amb_start=float(humidity[i]),
|
|
539
|
-
ua_btu_hr_f=loss.ua_btu_hr_f,
|
|
540
|
-
ua_exponent=loss.ua_exponent,
|
|
541
|
-
ua_ref_delta_t=loss.ua_ref_delta_t_f,
|
|
542
|
-
stratification_gap=float(gap[i]),
|
|
543
|
-
two_zone=two_zone,
|
|
544
|
-
tank=tank,
|
|
545
|
-
).minutes
|
|
546
|
-
for i in range(len(start))
|
|
547
|
-
]
|
|
576
|
+
return recovery_minutes(
|
|
577
|
+
frame,
|
|
578
|
+
k,
|
|
579
|
+
approach,
|
|
580
|
+
compressor=compressor,
|
|
581
|
+
loss=loss,
|
|
582
|
+
tank=tank,
|
|
583
|
+
two_zone=two_zone,
|
|
584
|
+
gaps=gaps,
|
|
548
585
|
)
|
|
549
586
|
|
|
550
|
-
|
|
587
|
+
actual = np.asarray(fit_df["duration_minutes"], dtype=float)
|
|
551
588
|
|
|
552
589
|
def objective(x: np.ndarray) -> float:
|
|
553
590
|
k, approach = float(x[0]), float(x[1])
|
|
@@ -556,9 +593,7 @@ def fit_compressor(
|
|
|
556
593
|
and APPROACH_BOUNDS[0] <= approach <= APPROACH_BOUNDS[1]
|
|
557
594
|
):
|
|
558
595
|
return 1e12
|
|
559
|
-
return float(
|
|
560
|
-
np.sum((predicted(fit_cols, k, approach) - fit_cols[5]) ** 2)
|
|
561
|
-
)
|
|
596
|
+
return float(np.sum((predicted(fit_df, k, approach) - actual) ** 2))
|
|
562
597
|
|
|
563
598
|
found = minimize(
|
|
564
599
|
objective,
|
|
@@ -573,15 +608,22 @@ def fit_compressor(
|
|
|
573
608
|
for value, bounds in ((k, K_BOUNDS), (approach, APPROACH_BOUNDS))
|
|
574
609
|
for bound in bounds
|
|
575
610
|
)
|
|
576
|
-
mae = float(np.mean(np.abs(predicted(
|
|
611
|
+
mae = float(np.mean(np.abs(predicted(fit_df, k, approach) - actual)))
|
|
577
612
|
held_mae = None
|
|
578
613
|
if n_held:
|
|
579
614
|
# The held-out rows' gaps as recorded, wherever the column is.
|
|
580
|
-
held_cols = columns(
|
|
581
|
-
held_df, gaps="stratification_gap" in held_df.columns
|
|
582
|
-
)
|
|
583
615
|
held_mae = float(
|
|
584
|
-
np.mean(
|
|
616
|
+
np.mean(
|
|
617
|
+
np.abs(
|
|
618
|
+
predicted(
|
|
619
|
+
held_df,
|
|
620
|
+
k,
|
|
621
|
+
approach,
|
|
622
|
+
gaps="stratification_gap" in held_df.columns,
|
|
623
|
+
)
|
|
624
|
+
- np.asarray(held_df["duration_minutes"], dtype=float)
|
|
625
|
+
)
|
|
626
|
+
)
|
|
585
627
|
)
|
|
586
628
|
return CompressorFit(
|
|
587
629
|
compressor_constant=k,
|
|
@@ -954,8 +996,13 @@ def with_standby(params: HeaterParams, fit: StandbyFit) -> HeaterParams:
|
|
|
954
996
|
"""``params`` with the standby fit's values where it had them."""
|
|
955
997
|
loss, tank = params.loss, params.tank
|
|
956
998
|
if fit.ua_ref is not None and fit.ua_exponent is not None:
|
|
999
|
+
# Quoted at the ΔT ``fit_standby``'s law was fitted at, not
|
|
1000
|
+
# whatever ``params`` held.
|
|
957
1001
|
loss = replace(
|
|
958
|
-
loss,
|
|
1002
|
+
loss,
|
|
1003
|
+
ua_btu_hr_f=fit.ua_ref,
|
|
1004
|
+
ua_exponent=fit.ua_exponent,
|
|
1005
|
+
ua_ref_delta_t_f=UA_REF_DELTA_T_F,
|
|
959
1006
|
)
|
|
960
1007
|
if fit.tau_gap_hours is not None:
|
|
961
1008
|
tank = replace(tank, stratification_gap_tau_h=fit.tau_gap_hours)
|
|
@@ -1060,15 +1107,14 @@ class PowerFit:
|
|
|
1060
1107
|
n_cop: int
|
|
1061
1108
|
|
|
1062
1109
|
|
|
1063
|
-
def
|
|
1110
|
+
def power_cycles(
|
|
1064
1111
|
rows: pd.DataFrame, params: HeaterParams
|
|
1065
|
-
) -> tuple[
|
|
1066
|
-
"""``(metered
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
n = 0
|
|
1112
|
+
) -> list[tuple[datetime | None, float, float]]:
|
|
1113
|
+
"""Each cycle's ``(start, metered, modelled)`` Wh for the power scale
|
|
1114
|
+
(``start`` None where the rows carry no ``start_time``):
|
|
1115
|
+
the integrator's own mean power over the cycle's real duration, so
|
|
1116
|
+
both sides are energy over the same clock."""
|
|
1117
|
+
out: list[tuple[datetime | None, float, float]] = []
|
|
1072
1118
|
for row in _records(_starts(rows)):
|
|
1073
1119
|
raw = row["metered_input_wh"]
|
|
1074
1120
|
if raw is None or pd.isna(raw):
|
|
@@ -1085,9 +1131,19 @@ def fit_power_scale(
|
|
|
1085
1131
|
modelled = power_w * real_minutes / 60
|
|
1086
1132
|
if not math.isfinite(modelled) or modelled <= 0.0:
|
|
1087
1133
|
continue
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1134
|
+
out.append((row.get("start_time"), metered, modelled))
|
|
1135
|
+
return out
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def fit_power_scale(
|
|
1139
|
+
rows: pd.DataFrame, params: HeaterParams
|
|
1140
|
+
) -> tuple[float | None, int]:
|
|
1141
|
+
"""``(metered / modelled, cycles)`` over ``power_cycles``, a ratio
|
|
1142
|
+
of sums. ``(None, n)`` with fewer than ``MIN_POWER_EVENTS``."""
|
|
1143
|
+
cycles = power_cycles(rows, params)
|
|
1144
|
+
metered_total = sum(m for _, m, _ in cycles)
|
|
1145
|
+
modelled_total = sum(e for _, _, e in cycles)
|
|
1146
|
+
n = len(cycles)
|
|
1091
1147
|
if n < MIN_POWER_EVENTS or modelled_total <= 0.0:
|
|
1092
1148
|
return None, n
|
|
1093
1149
|
return metered_total / modelled_total, n
|
|
@@ -1278,24 +1334,29 @@ def draw_free_withdrawn(row: Mapping[str, Any]) -> float | None:
|
|
|
1278
1334
|
return 0.0 if bool(row.get("draw_free")) else None
|
|
1279
1335
|
|
|
1280
1336
|
|
|
1281
|
-
|
|
1337
|
+
@dataclass(frozen=True)
|
|
1338
|
+
class CopCycle:
|
|
1339
|
+
"""One cycle's figures for the apparent COP: when it started, the
|
|
1340
|
+
heat it stored plus what draws took, the lead draws' heat, the
|
|
1341
|
+
integrator's COP, the metered energy (Wh) and the lift (°F).
|
|
1342
|
+
``start`` is None where the rows carry no ``start_time``."""
|
|
1343
|
+
|
|
1344
|
+
start: datetime | None
|
|
1345
|
+
heat_wh: float
|
|
1346
|
+
lead_wh: float
|
|
1347
|
+
modelled_cop: float
|
|
1348
|
+
metered_wh: float
|
|
1349
|
+
lift_f: float
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def cop_cycles(
|
|
1282
1353
|
rows: pd.DataFrame,
|
|
1283
1354
|
params: HeaterParams,
|
|
1284
1355
|
*,
|
|
1285
1356
|
withdrawn: Withdrawn = draw_free_withdrawn,
|
|
1286
1357
|
lead: Withdrawn | None = None,
|
|
1287
|
-
) ->
|
|
1288
|
-
"""``
|
|
1289
|
-
apparent COP, the heat a cycle stored (the full tank's mass times
|
|
1290
|
-
the rise of the probes' mean) plus what ``withdrawn`` says draws
|
|
1291
|
-
took during it, and ``lead`` those just before it, whose cold water
|
|
1292
|
-
the start reading had not yet seen. ``(None, n)`` with fewer than
|
|
1293
|
-
``MIN_POWER_EVENTS``.
|
|
1294
|
-
|
|
1295
|
-
Without a meter the default keeps only the cycles no water was
|
|
1296
|
-
drawn around: the heat drawn during a cycle is otherwise missing
|
|
1297
|
-
from its heat and pulls the scale low.
|
|
1298
|
-
"""
|
|
1358
|
+
) -> list[CopCycle]:
|
|
1359
|
+
"""The cycles ``fit_cop_scale`` fits on, and their figures."""
|
|
1299
1360
|
needed = [
|
|
1300
1361
|
"metered_input_wh",
|
|
1301
1362
|
"t_start_lower",
|
|
@@ -1304,13 +1365,9 @@ def fit_cop_scale(
|
|
|
1304
1365
|
"t_upper_end",
|
|
1305
1366
|
]
|
|
1306
1367
|
if not set(needed) <= set(rows.columns):
|
|
1307
|
-
return
|
|
1368
|
+
return []
|
|
1308
1369
|
wh_per_f = params.tank.thermal_mass_btu_f / BTU_PER_WH
|
|
1309
|
-
|
|
1310
|
-
before: list[float] = []
|
|
1311
|
-
cop: list[float] = []
|
|
1312
|
-
metered_wh: list[float] = []
|
|
1313
|
-
lift: list[float] = []
|
|
1370
|
+
out: list[CopCycle] = []
|
|
1314
1371
|
for row in _records(_starts(rows)):
|
|
1315
1372
|
if any(row[c] is None or pd.isna(row[c]) for c in needed):
|
|
1316
1373
|
continue
|
|
@@ -1338,6 +1395,7 @@ def fit_cop_scale(
|
|
|
1338
1395
|
path.elapsed_s[-1],
|
|
1339
1396
|
params.compressor.two_zone,
|
|
1340
1397
|
float(row["t_start_upper"]),
|
|
1398
|
+
tank=params.tank,
|
|
1341
1399
|
)
|
|
1342
1400
|
if modelled is None or not math.isfinite(modelled) or modelled <= 0:
|
|
1343
1401
|
continue
|
|
@@ -1350,15 +1408,51 @@ def fit_cop_scale(
|
|
|
1350
1408
|
+ float(row["t_start_lower"])
|
|
1351
1409
|
+ float(row["t_start_upper"])
|
|
1352
1410
|
) / 4
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1411
|
+
out.append(
|
|
1412
|
+
CopCycle(
|
|
1413
|
+
start=row.get("start_time"),
|
|
1414
|
+
heat_wh=stored + took,
|
|
1415
|
+
lead_wh=lead_wh,
|
|
1416
|
+
modelled_cop=modelled,
|
|
1417
|
+
metered_wh=metered,
|
|
1418
|
+
lift_f=mean - float(air),
|
|
1419
|
+
)
|
|
1420
|
+
)
|
|
1421
|
+
return out
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def fit_cop_scale(
|
|
1425
|
+
rows: pd.DataFrame,
|
|
1426
|
+
params: HeaterParams,
|
|
1427
|
+
*,
|
|
1428
|
+
withdrawn: Withdrawn = draw_free_withdrawn,
|
|
1429
|
+
lead: Withdrawn | None = None,
|
|
1430
|
+
) -> tuple[tuple[float, float, float, float] | None, int]:
|
|
1431
|
+
"""``((scale, startup_wh, lead_share, lift_coeff), cycles)`` for the
|
|
1432
|
+
apparent COP, the heat a cycle stored (the full tank's mass times
|
|
1433
|
+
the rise of the probes' mean) plus what ``withdrawn`` says draws
|
|
1434
|
+
took during it, and ``lead`` those just before it, whose cold water
|
|
1435
|
+
the start reading had not yet seen (``cop_cycles``). ``(None, n)``
|
|
1436
|
+
with fewer than ``MIN_POWER_EVENTS``.
|
|
1437
|
+
|
|
1438
|
+
Without a meter the default keeps only the cycles no water was
|
|
1439
|
+
drawn around: the heat drawn during a cycle is otherwise missing
|
|
1440
|
+
from its heat and pulls the scale low.
|
|
1441
|
+
"""
|
|
1442
|
+
cycles = cop_cycles(rows, params, withdrawn=withdrawn, lead=lead)
|
|
1443
|
+
n = len(cycles)
|
|
1359
1444
|
if n < MIN_POWER_EVENTS:
|
|
1360
1445
|
return None, n
|
|
1361
|
-
return
|
|
1446
|
+
return (
|
|
1447
|
+
solve_cop_fit(
|
|
1448
|
+
[c.heat_wh for c in cycles],
|
|
1449
|
+
[c.modelled_cop for c in cycles],
|
|
1450
|
+
[c.metered_wh for c in cycles],
|
|
1451
|
+
[c.lead_wh for c in cycles],
|
|
1452
|
+
[c.lift_f for c in cycles],
|
|
1453
|
+
),
|
|
1454
|
+
n,
|
|
1455
|
+
)
|
|
1362
1456
|
|
|
1363
1457
|
|
|
1364
1458
|
def fit_power(
|