569 строк · в начало
using Rugrid.Sdaid.Notation.Documents;
namespace Rugrid.Sdaid.Epas.Ntr.Backend.Calc
{
/// <summary>
/// Контракт модели расчёта: InputModel, OutputModel, CalcLog.
/// Перенесено дословно из ntr-01/in-works/spec-general-arc.md, разделы 2.1–2.3.
/// Согласованное исключение из правила «in-works/ игнорируем»: другого описания
/// контракта в источнике нет, а маппинги ссылаются на него.
/// </summary>
public class DOC_CalcModel : ISdaidDocument, IFolder<NtrBackendFolder>
{
public static string _refName = "Модель расчёта CalcModel";
public string Name => "2. Модель расчёта CalcModel";
public string Description => "Контракт модели расчёта: InputModel (единая модель ТЭ + ЭЭ), OutputModel (расчётные параметры МОП), CalcLog.";
public string Version => "1.2";
public string Status => "changed";
public string[] Comments => new[]
{
"2026-08-31, CR «Расчёт площади ВОК для четырёхскатных крыш»: в блок `roof` модели входных данных добавлена длина конька `ridgeLengthM`.",
"2026-09-03, CR «Обработка источников климатических данных»: широта `latitudeDeg` перенесена в блок `heatingPeriod` — она определяется выбранным источником климатических данных, а не зданием; блок `building` из TeInputSection убран, других параметров в нём не было.",
};
public static string Body = """"
```JSON
CalcModel {
context: {
idMap: [
{ refId: string, serviceId: uuid }
]
},
inputModel: InputModel,
outputModel: OutputModel,
calcLog: CalcLog
}
```
`context.idMap` — единообразный массив пар `{ refId, serviceId }`, связывающий внутренние идентификаторы модуля расчёта с идентификаторами сущностей сервиса в `domain-data.json`.
Синглтоны (одна сущность на модель, `refId` — фиксированное well-known имя):
| refId | Сущность сервиса | Коллекция в `domain-data.json` |
| --------------------- | ------------------------ | ------------------------------ |
| `consumptionModel` | МодельОбъектаПотребления | `consumptionModels` |
| `building` | Здание | `buildings` |
| `geometry` | Форма и геометрия | `buildingGeometry` |
| `walls` | Стены | `walls` |
| `windowModel` | Модель окон | `windowModels` |
| `doorModel` | Модель ворот | `doorModels` |
| `roof` | ВОК | `roofs` |
| `groundContact` | ЗКГ | `groundContacts` |
| `ventilation` | Вентиляция | `ventilations` |
| `operatingConditions` | Условия эксплуатации | `operatingConditions` |
| `heatingPeriod` | Условия ОП | `heatingPeriodConditions` |
| `lightingModel` | Модель освещения ЭЭ | `lightingModels` |
| `receiverModel` | Модель токоприёмников ЭЭ | `receiverModels` |
Коллекции (несколько элементов на модель, `refId` генерируется в `PrepareInputModel`):
| refId (шаблон) | Сущность сервиса | Коллекция в `domain-data.json` |
| ------------------ | ------------------------------ | ------------------------------ |
| `windowKind-N` | Вид остекления проёмов | `windows` |
| `doorKind-N` | Вид воротных проёмов | `doors` |
| `lightingKind-N` | Вид освещения | `buildingLighting` |
| `receiverKind-N` | Вид токоприёмника | `buildingEEConsumers` |
### 2.1 InputModel (единая модель ТЭ + ЭЭ)
Единое правило для `PrepareInputModel`: если в расчёт подставляется значение из справочника, передаются:
- код записи-источника (`*Code`);
- само значение в формате `ValueObject`.
```JSON
InputModel {
te: TeInputSection,
ee: EeInputSection
}
```
`InputModel` не содержит сервисных идентификаторов. Сущности внутри `InputModel` идентифицируются через `refId` (см. §2).
#### 2.1.1 `te: TeInputSection`
```JSON
TeInputSection {
geometry: {
buildingFormCode: string,
orientationSideACode: string,
buildingHeightM: ValueObject,
buildingFloorCount: ValueObject,
upperFloorLevelM: ValueObject,
heatedVolumeM3: ValueObject,
heatedAreaM2: ValueObject,
dimensionsM: {
A: ValueObject,
B: ValueObject,
C: ValueObject,
D: ValueObject,
E: ValueObject,
F: ValueObject,
G: ValueObject,
H: ValueObject
}
},
walls: {
material: {
materialCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
},
mainInsulation: {
insulationCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
},
additionalInsulation: {
insulationCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
}
},
roof: {
roofTypeCode: string,
hasAttic: boolean,
ridgeLengthM: ValueObject | null,
customRoofAreaM2: ValueObject | null,
customAtticWallAreaM2: ValueObject | null,
floorLayer: {
materialCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
},
floorInsulation: {
insulationCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
},
roofInsulation: {
insulationCode: string,
thicknessMm: ValueObject,
thermalConductivity: ValueObject
}
},
groundContact: {
hasBasement: boolean,
basementCeilingHeightM: ValueObject | null,
basementFloorLevelM: ValueObject | null,
customGroundContactAreaM2: ValueObject | null
},
windows: {
items: [
{
refId: string,
openingAreaM2: ValueObject,
openingsBySide: {
A: ValueObject,
B: ValueObject,
C: ValueObject,
D: ValueObject,
E: ValueObject,
F: ValueObject,
G: ValueObject,
H: ValueObject
},
thermalResistance: ValueObject,
shadingCoefficient: ValueObject,
solarRadiationCoefficient: ValueObject,
airPermeabilityAt10Pa: ValueObject
}
]
},
doors: {
items: [
{
refId: string,
openingAreaM2: ValueObject,
openingsBySide: {
A: ValueObject,
B: ValueObject,
C: ValueObject,
D: ValueObject,
E: ValueObject,
F: ValueObject,
G: ValueObject,
H: ValueObject
},
thermalResistance: ValueObject,
airPermeabilityAt10Pa: ValueObject
}
]
},
ventilation: {
hasMechanicalVentilation: boolean,
hasRecuperator: boolean,
recuperatorEfficiency: ValueObject | null
},
operating: {
workplacesCount: ValueObject,
internalTemperatureC: ValueObject,
buildingPurposeCode: string,
airConsumptionM3PerPersonHour: ValueObject,
heatingRegulationTypeCode: string,
kReg: ValueObject,
energySupplyTypeCode: string
},
heatingPeriod: {
durationDays: ValueObject,
externalTemperatureC: ValueObject,
windSpeedMps: ValueObject,
latitudeDeg: ValueObject
},
solarRadiation: {
items: [
{
directionCode: string,
monthNumber: number,
radiation: ValueObject
}
]
},
normative: {
specificHeatConsumptionNorm: {
recordCode: string,
valueObject: ValueObject
},
energyEfficiencyClasses: [
{
code: string,
deviationMinPercent: ValueObject,
deviationMaxPercent: ValueObject
}
]
},
coefficients: {
surfaceThermalResistance: {
code: string,
valueObject: ValueObject
},
airSpecificHeat: {
code: string,
valueObject: ValueObject
},
reductionBuildingsAirVolume: {
code: string,
valueObject: ValueObject
},
workingTimePerDayH: {
code: string,
valueObject: ValueObject
},
basementZonesThermalResistance: {
I: {
code: string,
valueObject: ValueObject
},
II: {
code: string,
valueObject: ValueObject
},
III: {
code: string,
valueObject: ValueObject
},
IV: {
code: string,
valueObject: ValueObject
}
}
}
}
```
Число в `te.solarRadiation.items[].radiation` берётся только из поля `value` записи справочника (`solarRadiationData` / `solarRadiationRecord` в `domain-schema.json`). В корректном наполнении справочника однозначно задаётся кортеж (сторона света, нижняя граница широтного диапазона в градусах северной широты, номер месяца); в массиве `items` для выбранного диапазона широты здания не ожидается больше одного элемента с одной и той же парой (`directionCode`, `monthNumber`).
#### 2.1.2 `ee: EeInputSection`
```JSON
EeInputSection {
lighting: {
items: [
{
refId: string,
fixturesCount: ValueObject,
lampsPerFixture: ValueObject,
lampPowerW: ValueObject,
hoursPerDay: ValueObject,
daysPerYear: ValueObject
}
]
},
receivers: {
items: [
{
refId: string,
totalPowerKw: ValueObject,
hoursPerDay: ValueObject,
daysPerYear: ValueObject
}
]
}
}
```
### 2.2 OutputModel (расчётные параметры МОП)
```JSON
OutputModel {
te: TeOutputSection,
ee: EeOutputSection,
totals: CommonTotals
}
```
#### 2.2.1 `te: TeOutputSection`
Все расчётные поля ТЭ задаются в формате `ValueObject`.
```JSON
TeOutputSection {
energyEfficiencyClass: {
classCode: string,
normativeSpecificCharacteristic: ValueObject,
calcSpecificCharacteristic: ValueObject,
deviationPercent: ValueObject
},
keBuilding: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject
},
keWalls: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
overlapThicknessBySideM: {
A: ValueObject, B: ValueObject, C: ValueObject, D: ValueObject,
E: ValueObject, F: ValueObject, G: ValueObject, H: ValueObject
},
innerPerimeterM: ValueObject,
innerSurfaceAreaM2: ValueObject
},
keWindows: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
airPermeabilityResistance: ValueObject,
branch: { formMethodSectionId: string, orientationMethodSectionId: string },
areaBySide: { A: ValueObject, B: ValueObject, C: ValueObject, D: ValueObject, E: ValueObject, F: ValueObject, G: ValueObject, H: ValueObject },
tempSidesS1ToS4: { s1: ValueObject, s2: ValueObject, s3: ValueObject, s4: ValueObject },
areaByDirection: { north: ValueObject, east: ValueObject, south: ValueObject, west: ValueObject },
solarRadiation: {
byDirection: [
{ directionCode: string, value: ValueObject }
]
},
items: [{
refId: string,
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
airPermeabilityResistance: ValueObject,
infiltrationAirKgH: ValueObject
}]
},
keDoors: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
airPermeabilityResistance: ValueObject,
branch: { formMethodSectionId: string, orientationMethodSectionId: string },
areaBySide: { A: ValueObject, B: ValueObject, C: ValueObject, D: ValueObject, E: ValueObject, F: ValueObject, G: ValueObject, H: ValueObject },
tempSidesS1ToS4: { s1: ValueObject, s2: ValueObject, s3: ValueObject, s4: ValueObject },
areaByDirection: { north: ValueObject, east: ValueObject, south: ValueObject, west: ValueObject },
items: [{
refId: string,
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
airPermeabilityResistance: ValueObject,
infiltrationAirKgH: ValueObject
}]
},
keUpperEnclosures: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
atticWallAreaM2: ValueObject,
roofCoverAreaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
branch: { atticWallMethodSectionId: string, roofCoverMethodSectionId: string }
},
keGroundContact: {
specHeatProtectChar: ValueObject,
areaM2: ValueObject,
basementWallAreaM2: ValueObject,
floorAreaM2: ValueObject,
thermalResistance: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
branch: { basementWallMethodSectionId: string, floorAreaMethodSectionId: string, areaMethodSectionId: string, zonesFormMethodSectionId: string },
zonesAreaM2: { I: ValueObject, II: ValueObject, III: ValueObject, IV: ValueObject },
zoneSideLengths: object
},
solarRadiation: {
byDirection: [
{ directionCode: string, value: ValueObject }
]
},
ventilation: {
specVentChar: ValueObject,
heatLossEnergyThousandKwh: ValueObject,
heatLossPowerKw: ValueObject,
infiltrationAirMassTotal: ValueObject,
supplyAirFlowM3H: ValueObject,
ventilationHours: ValueObject,
recuperatorEfficiency: ValueObject,
infiltrationAirKgH: ValueObject,
infiltrationHours: ValueObject,
pressureDifferencePa: ValueObject,
airWeightOutside: ValueObject,
airWeightInside: ValueObject,
branch: { ventHoursMethodSectionId: string, recuperatorMethodSectionId: string, infiltrationHoursMethodSectionId: string }
},
heatGains: {
specHeatGainsChar: ValueObject,
heatGainsEnergyTotal: ValueObject,
heatGainsPowerTotal: ValueObject,
specDomesticHeatGainsChar: ValueObject,
domesticHeatGainsEnergy: ValueObject,
domesticHeatGainsPower: ValueObject,
specSolarHeatGainsChar: ValueObject,
solarHeatGainsEnergy: ValueObject,
solarHeatGainsPower: ValueObject,
coefHeatGainsEfficiency: ValueObject
},
heatingPeriod: {
degreeDays: ValueObject
}
}
```
#### 2.2.2 `ee: EeOutputSection`
```JSON
EeOutputSection {
lighting: {
items: [{ refId: string, installedPowerKw: ValueObject, annualConsumptionThousandKwh: ValueObject }],
installedPowerKw: ValueObject,
annualConsumptionThousandKwh: ValueObject
},
receivers: {
items: [{ refId: string, installedPowerKw: ValueObject, annualConsumptionThousandKwh: ValueObject }],
installedPowerKw: ValueObject,
annualConsumptionThousandKwh: ValueObject
},
subtotal: {
installedPowerKw: ValueObject,
annualConsumptionThousandKwh: ValueObject
}
}
```
#### 2.2.3 `totals: CommonTotals`
```JSON
CommonTotals {
eeWithoutHeating: {
installedPowerKw: ValueObject,
annualConsumptionThousandKwh: ValueObject
},
electricHeating: {
enabled: boolean,
annualConsumptionThousandKwh: ValueObject
},
eeTotal: {
annualConsumptionThousandKwh: ValueObject
},
teTotal: {
annualConsumptionThousandKwh: ValueObject
}
}
```
Правила агрегации итогов не дублируются в этом документе и задаются только в методиках:
- `ttr-service/domain/ttr-calc.md`
- `ttr-service/domain/ee_calc.md`
### 2.3 CalcLog
#### 2.3.1 Контракт `CalcLog` и записей `CalcLogEntry`
Оркестратор `ModelCalc` формирует журнал шагов расчёта. Каждый шаг — одна запись `CalcLogEntry` на вызов одной функции расчёта (`functionId`).
```JSON
CalcLog {
startedAt: string,
finishedAt: string,
records: CalcLogEntry[],
errorsCount: ValueObject,
warningsCount: ValueObject
}
```
Структура `CalcLogEntry` **одинакова** для любого статуса:
```JSON
CalcLogEntry {
functionId: string,
methodSectionId: string,
status: "ok" | "missing_input" | "error" | "not_found",
inputs: object,
outputs: object,
formulaTemplate: string,
formulaSubstitution: object,
missingInputs: string[]
}
```
- `inputs` / `outputs` — объекты с привязкой полей к обозначениям методики и/или путям `InputModel` / `OutputModel` (как в карточах §4.3).
- `formulaTemplate` — шаблон формулы (в т.ч. LaTeX), с плейсхолдерами под подстановку значений из `formulaSubstitution`.
- `formulaSubstitution` — словарь «символ → подставленное значение» (число, строка с единицами, или `"-"` при отсутствии входа).
Правила формирования `formulaSubstitution`:
- заполняется на основе карточки функции (§4.3): обозначения берутся из `Входные параметры` и/или `Правило расчёта`;
- должен содержать значения всех входных параметров функции;
- для каждого параметра подставляется фактическое значение или `"-"` при отсутствии значения;
- если входной параметр — массив `X`, используется формат `X[key] = value`;
- если элементов массива `<= 10`, указываются все элементы;
- если элементов массива `> 10`, указываются первые 10 элементов и затем `"..."`.
**Правила при `status = missing_input`:**
- функция не выполняется;
- `inputs` и `outputs` в записи остаются (не очищаются);
- в `formulaSubstitution` для отсутствующих параметров подставляется `"-"`;
- `missingInputs` содержит список путей параметров, из‑за которых шаг не выполнен.
#### 2.3.2 Подсказка к значению расчётного показателя (UI)
Для отображаемого в интерфейсе значения поля `OutputModel` с путём `outputPath`:
1. **Найти запись журнала:** выбрать `CalcLogEntry` из `records`, в которой в `outputs` присутствует расчётный результат для `outputPath` (или согласованное с оркестратором правило «последняя запись, записавшая этот путь»). Если таких несколько (агрегация), UI следует правилам продукта, зафиксированным в отдельном требовании к экрану.
2. **Собрать текст подсказки:**
- взять `formulaTemplate`;
- подставить значения из `formulaSubstitution` вместо символов (включая отображение `"-"` для пропусков);
- при необходимости добавить `methodSectionId` / ссылку на фрагмент методики (ТЭ или ЭЭ).
3. **При `missing_input`:** показать, что значение не получено, и вывести `missingInputs` (через запятую или списком), без выдумывания чисел в UI.
"""";
}
}