INFO: Studie über SFML Prognosmöglichkeiten (Statisch, Rollend, ..)

Technischer Einblick: Diskussion zur Prognosestruktur in SFML + TFS

Abseits der reinen Entwicklung versuche ich regelmäßig, den Blick hinter die Kulissen zu ermöglichen und die technische Logik der Software zu erläutern, spannende Themen zu teilen.
Aktuell führt ein GitHub-Nutzer in den USA eine Teststudie durch, die eine interessante Diskussion über die Bereitstellung der Daten angestoßen hat.

Die aktuelle Systematik

SFML + TFS ist per Definition eine statische Prognose. Technisch gesehen verarbeitet die lokale KI jedoch bereits jetzt eine größere Menge an Datenpunkten.
Die Diskussion begann mit dem Vorschlag des Nutzers, statt der bisherigen Form drei verschiedene Prognosen parallel anzubieten, damit der Anwender je nach Bedarf entscheiden kann, welche für das eigene Szenario relevant ist.
Im Code sind diese Varianten bereits enthalten und in den LOG-Files abgebildet… (seit der BETA)

In einem technischen Austausch via Teams bin ich mit dem Nutzer den Code durchgegangen, um zu besprechen, was die KI derzeit leistet /technisch lokal möglich ist. Dabei wurden auch Randthemen wie die Einbettung lokaler LLMs besprochen, was jedoch ein separates Thema ist und bleibt.

Der aktuelle Stand im Software-Stack:

  • Statische Prognose: Vollständig ausgerollt.
  • P10 Blend Prognose: Vollständig ausgerollt.
  • Rollende Prognose: Im Code vorhanden, wird von mir aktuell jedoch ausschließlich zum Lernen der KI genutzt.

Die Untersuchung

Ich habe den Software-Stack für den Tester in den USA angepasst, sodass er nun die Unterschiede zwischen den einzelnen Prognosetypen und einen möglichen Mehrwert untersuchen kann. Es geht dabei um den direkten Vergleich der Ergebnisse im Live-Betrieb.


Eure Meinung dazu:

Würdet ihr den Einsatz von drei verschiedenen Prognose-Sensoren befürworten, die – ähnlich wie bei Diensten wie Solcast oder Forecast.Solar – permanent Daten „nachschieben /rollend zu korrigieren“?

  1. Sensor: Statisch
  2. Sensor: P10 Blend
  3. Sensor: Rollend wie Solcast und Co

Hat diese Redundanz und die ständige Aktualisierung für euch einen praktischen Nutzen?

Zara

Also ich persönlich präferiere ein fixes Modell wie du es jetzt hast.

Was bringt mir ein Modell, dass bis zum Abend hin ständig korrigiert - ich brauche für Automatisierungen und Ableitungen einen fixen Wert, der nicht ständig springt.

Mir ist es schon passiert (mit anderen Modellen), dass ich mit 50kWh gestartet bin, dann plötzlich zu Mittag ein Abfall auf 10kWh (der hat gleich sämtliche Automatisierungen getriggert) und dann 1h später stieg er wieder auf 40kWh hoch.

Damit kann ich persönlich nichts anfangen…ich arbeite immer mit den Tages-Startwerten und eine Korrektur zu Mittag reicht mir völlig für Autoamtisierungen, die am Nachmittag starten (Warmwasser bei noch genügend Sonnenschein - aber so spät wie möglich usw…).

1 „Gefällt mir“

Das ist ein Interessanter Punkt - der noch eine andere Option eröffnet.. einen Toggle den der User bewusst aktivieren muss und dann würde z.B. um 12 Uhr noch einmal eine Prognose ausgelöst für den Rest des Tages! -oder zur Hälfte des Produktionstages.. spannend! Ein Hybrid aus beiden Welten

1 „Gefällt mir“

Zum Beispiel ja. Ich habe einige Trigger, die am Forecast hängen…

) Warmwasser (will ich so spät wie möglich machen, da wir immer am Abend duschen und je früher ich es mache, desto heißer muss ich es machen, damit ich am Abend immer die gleiche Temp habe)

) Pool Wärmepumpe (je nach Forecastwird Vollgas geheizt oder nur im Quiet Mode

) Einspeisung: Wenn genug PV runterkommt, entlade ich die BAtterie in der Nacht komplett ins Netz (bin in einer Energiegemeinschaft und wir möchten so autark wie möglich sein)

) Heizung: Je nachdem wie viel PV runterkommt überheize ich das Haus leicht und sperre die WP in der Nacht. Weiß ich, dass am nächsten Tag genug PV runterkommt, entlade ich zusätzlich die Batterie weiter runter als normalerweise

Da brauche ich eine zuverlässige Vorhersage und auch deine T+1 Vorhersagen ändern sich nicht minütlich und sind auch heute für Morgen bereits sehr gut.

Genau das, ein bis zwei zeitlich flexibel triggerbare Zeiten hatte ich vor einigen Tagen als Vorschlag eingebracht - sprich, dieser Zwitter/Hybrid wäre auch mein Favorit :grinning_face:

Gefunden:

1 „Gefällt mir“

@Twix1212 @lemuba
Stimmt, das hattest du getan! Das Problem an der ganzen Geschichte ist nur, dass es ein sehr sehr großer Umbau ist! Denn unter keinen Umständen dürfen die Lerndateien von einem zusätzlichen Forecast in irgendeiner Art und Weise verändert werden. Ansonsten würde es dazu führen, dass die ki komplett falsch lernt und sich nie verbessert und ein Forstcast nur noch stimmen würde mit einem zusätzlichen Trigger. Das ist genau die schwäche der meisten am Markt befindlichen Forecast.
Ich will hier jetzt nicht zu technisch werden, aber ein Hybrid wäre eine komplette Neuentwicklung, die nicht ins lernen einfließen darf. Jetzt könnte man denken, dann schreib doch einfach in eine neue Tabelle. So einfach ist es aber nicht da die Forecast Logik die gleiche sein müsste die aktuell vorher herrscht d.h. ein Hybrid würde die statische Prognose vom Morgen inklusive aller damit zusammenhängender Lernparametern komplett verfälschen und überschreiben. Der nächste Punkt ist, wie soll das Ganze in Stats funktionieren auch das wäre ein kompletter großer Umbau! Aktuell finden alle Berechnungen auf der statischen Prognose und all ihren Parametern statt. Wenn jetzt ein separater Fahrt dazu kommt, erzeugt das eine Menge Probleme. Ich erkläre das nur, um klarzumachen, worüber wir hier eigentlich reden. Das ist keine Sache, die man mal eben schnell machen kann!

Eine rollende Prognose zu erstellen. Hingegen ist super simpel, da der Koordinator eh zu jeder vollen Stunde eine Prognose erstellt. Diese wird aktuell jedoch nur zum lernen genutzt. Der Sensor würde diese Prognose einfach nur anzeigen. Aber ihr habt beide recht das ist Unsinn! Die hybrid Lösung ist technisch sehr sehr anspruchsvoll darum macht das vermutlich auch niemand, aber sie ist auch die deutlich bessere Option.

Aus reiner Entwickler-Sicht wäre das Next-Level und ist ultra-komplx in der Umsetzung! Wir reden hier nicht über einen Wunsch, sondern einen heftigen Umbau von SFML, TFS, STATS, GPM… Ohje… ehrlich gesagt graut es mir davor! - Ich rechne mit mindestens 100 -200 Std arbeit ohne Testen… Es sei denn ich kann den Ami überreden zu forken.. (umsonst und aus Spaß an der Freunde) er kann coden wir es segemtieren könnten… aber ich müsste sicherstellen das er keine KI nutzt… das ist mein größer Bauchschmerz.. ich will KEINE KI-Slops ect in meinem CODE!!!

Zu deinem Punkt Abweichung… daür ist der P10 Blend… der ist verlässlich alles anders ist “übern Durst”… das hat also im weitesten Sinne nichts mit dem Hybrid zu tun.

2 „Gefällt mir“

Das ist interessant, vor allem, da es gestern eine hitzige Diskussion darüber gab.

NEIN! Das ist ein völlig anderes Thema! Fang bitte nicht wieder an unfair zu werden! Dein Thema ist und bleibt: Du hast Entwickler-Services ausgelöst, trotz klarem Hinweis das sie destruktiv sind. Damit hast du deine KI, den Code, die DB zerstört. Des weiteren ist dein Thema das Du selbst gebastelte Karten nutzt, die nich über die notwendigen Daten verfügen um sauber zu tracken ob es überhaupt ein Problem gibt. Des Weiteren hast Du behaupte das STATS nicht auf PI funktioniert, was nicht der Wahrheit entspricht, seit einigen Updates.
Hinzu kommt auch noch das Du leider nicht verstanden hast, das keine Wetterdaten aus App`s geladen werden und das es sehr wohl einen Unterschied macht wo in der Luftsäule sich Strahlungshemmer befinden und das es nicht nur Wolken sind sondern auch Pollen, Luftverschmutzung und viel wichtiger, dass ein für und Menschen blauer Himmerl nicht gleichzeitig 100% Strahlung bedeutet… zum Schluss hast du dann noch deinen Helfer angeführt, der genauer ist als SFML und beschrieben wie du auf einem Blatt Papier und mit einem Faktor von 20% bei bewölung einen super Forcast mit einem HA Helfer erzeugst… Das alles hat mit dieser Diskussion nichts zu tun! Mal abgesehen davon, dass viele User versucht haben Dir zu helfen, dir Erklärungen geliefert haben, du Anleitungen und Screen-Shoots erhalten hast.
Auch wurdest Du gebten die Erklärungen zu lesen, wie was funktioinert und warum.. dazu noch ein reger Austausch via PN und zu guter letzt dann noch die Behauptung das ich / andere Lügen würden.. Sorry das geht nicht und ist undankbar, unfair und gegen jede Höfflichkeit.. daher habe ich auch deinen Mod gebeten einen Thread zu schließen.. wenn Du hier nun wieder anfängst Dinge zu vermischen, zu provozieren… ist das unredlich!
Genauso wie deine kontexlosen Kommentare und Angriffe auf Github… wenn das alles nicht deinen Anforderungen entspricht, die dir Unterstützung aller nicht reicht.. dann ist das okay aber dann solltest Du vieleicht eine andere App nutzen..

1 „Gefällt mir“

Ich hätte noch eine Idee: Könnte man nicht einfach die Prognose morgens zweimal erstellen lassen, also einfach nochmal eine Kopie davon? Die eine Prognose bleibt dann wie bisher und wird wie bisher zum Lernen verwendet und die andere wird im Laufe des Tages bei neuen, aktuelleren Wetterdaten immer wieder aktualisiert. Am Ende des Tages wird diese zweite Prognose dann einfach verworfen und nicht für Trainings oder so verwendet.

Ich konnte jedenfalls bei mir beobachten, dass große Abweichungen im Verlauf des Tages, meist durch falsche bzw. schlechte Wettervorhersagen zustandekommen. Beispiel: Laut Wettervorhersage Strahlung bei 700 W/m2, Realität aber stark bewölkt und wenig Sonnenschein.

Im Kern ist das genau der Weg, quasi eine weitere Tablle. Der Knackpunkt ist aber, das auch die KI an der Stelle getrennt werden muss, also auch alle damit zusammenhängenden Prozesse.. hier mal ein Schaubild, dass gut verdeutlicht wie eine Prognose entsteht.. all das muss aufgebrochen werden, dass kann nicht einfach dubliziert werden. Dann ist da auch noch das Probem mit der Hardware, es sind zusätzliche Resourchen die da benötigt werden… wie sagt man immer so schön " Es ist kompliziert!"

Stell dir / stellt euch die Linien wie Autobahnen vor und jeden Kontenpunkt ist eine Stadt in der Größe von Frankfurt und hat eine eigene Straßenkarte.. all dieses müsste angepasst werden ohne das etwas bricht.

Das Flow-Diagram ist allerdinge TFS und nich SFML.. SFML ist da noch viel komplexer durch das HA-Geraffel wie API, DB, Pollen,…

Oben links rein… unten rechts Prognose raus… :slight_smile:

okay, vielen Dank. Verstehe, dass das nicht so einfach geht. Ich finde das Bild verdeutlicht auch nochmal ganz gut, wie komplex das ganze System eigentlich ist :hushed_face:

1 „Gefällt mir“

NO!!! Please don’t mix topics. There were maybe two in parallel: one came out of another. There was a huge discrepancies between forecast and reality which I wanted to explain if it is caused by old weather data aka rolling forecast could help or there is something wrong with a model.

There were basically no feedback for over a month so I started to dig and most probably issue was with one of the update.

And since forecast after next month just started to stabilize I asked here for suggestion and started to check dev services.

You said that most probably destroyed database including the file that has komplety no information related to sfml model and is internal HA file.

Since I had rolling forecast in my mind and found discussion I add my two cents but you clearly stated that rolling forecast is not an option.

Regarding sfml stats - it is clearly stated on GitHub main page of the project that stats are only for x86 architecture!

Regarding forecast on a sheet of paper, again FOR CLEAR SKY it is basically trigonometry - it’s gaussian bell. And again 20% was used in completely different meaning but ok.

I’m unfair, uncivil and dishonest? Have a look on my first 2 posts regarding rolling forecast where I suggested what we have basically in this discussion here and you said it’s not gona happen and feel free and go if I want rolling forecast

Regarding GitHub topic, there was a many evidence that it is something very wrong with forecast, there was other people that experienced same case and yet you decided to close due to “no direct proof” meaning what?

Ja, und das ist nur der Prozess wie eine Prognose erstellt wird! Es gibt aber ja zusätzlich noch viele andern Data-Flow diagramme.. das sicherlich komplexeste ist der EOD das ist ca. 3x so groß! Wenn man dann auch noch die Wetter-KI und alle anderen Features danebenstellt.. ist das schon ein gewaltiges Konstrukt das vermutlich eher an 3D Schach erinnert als an eine Integration :slight_smile:

Ich werde mir aber trotzdem mal Gedanken machen und mit dem Kollegen aus den USA sprechen, ob er nicht unterstützen kann. Denkbar wäre z.B. ich mache mich an die KI und er an Stats.. so oder so ist es ein großer Umbau und berührt so ziemlich alles in den Integrationen.

1 „Gefällt mir“

Not to mention that after you heard that I compared your ML forecast - you triggered and there were totally no discussion

Please have a look than - this helper for last month provided more accurate forecast than your model

{% set P_nom = 6.5 %}{% set eta = 1 %}{% set beta = 40 %}{% set gamma_p = 201 %}

{% set main_set = 1.4 %}{% set min_ratio = 0.1 %}{% set sun_limit = 40 %}

{% set use_sun = false %}

{% set shades = [{‘az’: 261, ‘width’: 40, ‘height’: 25, ‘max’: 0.4},{‘az’: 120, ‘width’: 40, ‘height’: 40, ‘max’: 0.8}] %}

{% set theta_limit = 91 %}{% set min_frac = 0.005 %}

{% set phi = state_attr(‘zone.home’,‘latitude’) | float() %}{% set lon_geo = state_attr(‘zone.home’,‘longitude’) | float() %}{% set n = now().timetuple().tm_yday %}{% set pi = 3.141592653589793 %}

{% set beta_rad = beta * pi / 180 %}{% set gamma_p_rad = gamma_p * pi / 180 %}

{% set lon_std = 15 %}{% set B = (360/365)(n-81)pi/180 %}{% set EoT = 9.87sin(2B) - 7.53cos(B) - 1.5sin(B) %}

{% set forecast_raw = state_attr(‘sensor.home_energy_production_today’, ‘wh_period’) or {} %}{% set today = now().strftime(‘%Y-%m-%d’) %}

{% set ns = namespace(result=, total=0) %}

{% for h in range(24) %}

{# ================= SUN GEOMETRY ================= #}{% set t_solar = h + ((lon_std - lon_geo)4 + EoT)/60 %}{% set h_angle = 15(t_solar - 12) %}{% set h_angle_rad = h_angle * pi / 180 %}

{% set delta = 23.45 * sin((360/365)*(284+n) * pi/180) %}{% set delta_rad = delta * pi / 180 %}

{% set cos_theta_z = sin(phi*pi/180)sin(delta_rad) + cos(phipi/180)*cos(delta_rad)*cos(h_angle_rad) %}{% set theta_z = acos(cos_theta_z) %}{% set theta_z_deg = theta_z * 180/pi %}

{% set sin_az = cos(delta_rad)*sin(h_angle_rad)/sin(theta_z) %}{% set cos_az = (cos(theta_z)sin(phipi/180)-sin(delta_rad)) / (sin(theta_z)cos(phipi/180)) %}{% set azimuth_s = atan2(sin_az, cos_az) %}{% set azimuth_s_deg = (azimuth_s * 180 / pi + 180) % 360 %}

{# ================= INCIDENCE ================= #}{% set cos_theta_i = cos(theta_z)*cos(beta_rad) + sin(theta_z)*sin(beta_rad)cos((azimuth_s_degpi/180) - gamma_p_rad) %}

{# ================= DIRECT / DIFFUSE ================= #}{% set direct = P_nom * eta * max(cos_theta_i, 0) %}

{% if theta_z_deg < theta_limit %}{% set diffuse = P_nom * eta * min_frac * (theta_limit - theta_z_deg)/(theta_limit - 90) %}{% else %}{% set diffuse = 0 %}{% endif %}

{# ================= SHADING ================= #}{% set sun_elev = 90 - theta_z_deg %}{% set shade_total = namespace(v=0) %}

{% for s in shades %}
{% set delta_az = (azimuth_s_deg - s.az) %}

{% if delta_az > 180 %}
  {% set delta_az = delta_az - 360 %}
{% elif delta_az < -180 %}
  {% set delta_az = delta_az + 360 %}
{% endif %}

{% set delta_az = delta_az | abs %}

{% if delta_az < s.width and sun_elev < s.height %}
  {% set az_factor = 1 - (delta_az / s.width) %}
  {% set el_factor = 1 - (sun_elev / s.height) %}
  {% set shade_total.v = shade_total.v + (s.max * az_factor * el_factor) %}
{% endif %}
{% endfor %}

{% if shade_total.v > 0.9 %}{% set shade_total.v = 0.9 %}{% endif %}{% set direct_shaded = direct * (1 - shade_total.v) %}{% set max_h = max(direct_shaded, diffuse) %}

{# ================= FORECAST ================= #}{% set ts_key = today ~ ‘T%02d:00:00+02:00’ | format(h) %}{% set forecast_val = forecast_raw[ts_key] | default(0) / 1000 %}

{% if max_h > 0 %}{% set ratio = forecast_val / max_h %}{% set ratio = min(ratio, 1) %}{% else %}{% set ratio = 0 %}{% endif %}

{# ================= MODEL ================= #}

{% set r = [0, [ratio, 1] | min] | max %}

{% if r <= min_ratio %}{% set main_ratio = 1 %}{% else %}{% set t = (r - min_ratio) / (1 - min_ratio) %}{% set main_ratio = 1 + (main_set - 1) * t %}{% endif %}

{# <<< CHANGED: sun toggle #}{% if not use_sun %}{% set sun_t = 1 %}{% else %}{% if sun_elev >= sun_limit %}{% set sun_t = 1 %}{% elif sun_elev <= 0 %}{% set sun_t = 0 %}{% else %}{% set sun_t = sun_elev / sun_limit %}{% endif %}{% endif %}

{# <<< final main #}{% set main = 1 + (main_ratio - 1) * sun_t %}

{# <<< final formula #}{% set corrected = max_h - main * (max_h - forecast_val) %}

{# ================= CLEANUP ================= #}{% if corrected < 0.000001 %}{% set corrected = 0 %}{% elif corrected > max_h %}{% set corrected = max_h %}{% endif %}

{% set ns.total = ns.total + corrected %}{% set ns.result = ns.result + [h ~ “=” ~ (corrected | round(3))] %}

{% endfor %}

{{ ns.result | join(‘|’) ~ ‘|total=’ ~ (ns.total|round(3)) }}

And to be honest, I’m ok with situation when on open project like this, there are some issues. Even if I would destroy db with dev services… well ok, sometimes you need to start over. Although my first question was about editing db to exclude problematic period.

Ich bitte Dich jetzt wirklich ein letztes Mal, mit diesen themenfremden Einwürfen aufzuhören und endlich zu versuchen zu verstehen, was ich und andere Dir bereits mehrfach erklärt haben.

Bitte nutze zudem ein Übersetzungstool und schreibe in diesem Thread auf Deutsch. Dir wurde mehrfach angeboten, auf Englisch per PN weiterzuschreiben. Hier im Thread führt das nur weiter vom eigentlichen Thema weg. Ich antworte Dir dazu hier jetzt auch letztmalig, weil das inzwischen klar off-topic ist und ich nicht möchte, dass auch dieser Thread wieder wegen Deiner Nebenbaustellen geschlossen werden muss.

Der von Dir gepostete Helper ist kein belastbarer Gegenbeweis für SFML, sondern ein eigenes, manuell gebautes Vergleichsmodell mit eigenen Annahmen, Filtern und Korrekturen. Damit kann man für einen Einzelfall experimentieren, aber daraus lässt sich weder ein genereller Fehler in SFML noch ein sauberer Systemvergleich ableiten.

Wenn ein technischer Fehler in SFML belegt werden soll, brauche ich weiterhin reproduzierbare Rohdaten aus dem tatsächlichen SFML-Datenpfad:

  • betroffene Tage
  • Logauszug
  • relevante DB-Werte
  • Forecast-, IST- und Exclude-Flags auf Stundenbasis

Alles andere bleibt eine subjektive Vergleichsbeobachtung, aber keine belastbare Fehleranalyse.

3 „Gefällt mir“

Hallo Zara,
Hallo Forum,

Rollierend ‘light’ ist meines Wissens ja schon dabei. Der MDC könnte eventuell etwas feiner sein und mittags ggf schon früher getriggert werden
B.t.w.. für mich ist die statische Variante in seiner jetzigen Form voll genial und absolut ausreichend.
Danke Tom-Ha !!!

Grüße,
Dieter
..meinen kleinen PI habe ich zurück in die Bastelkiste gelegt und wurde durch einen Mini-PC ersetzt. Ich freue mich täglich auf STATS. Der Geekom X86 CPU zeigt mir Klasse Dashboard.

Ich sehe das genauso wie Twix1212, auch ich hatte mit anderen Vorhersagen experimentiert, aber niemals war es so stabil, das ich mich drauf verlassen konnte. Zumeist zeigten die immer viel zu optimistisch an.

Auch wenn SFML ab und an stark abweicht ist es immer noch wesentlich genauer als andere. Man muß aber auch immer sagen: Wetter ist Chaotisch. Gerade bei mir in der Lage habe ich viele Besonderheiten, die mir Wolken plötzlich umlenken - je nach Wind - und alles Prognosen - egal ob Wetter oder Solar - komplett über den Haufen werfen.

Und eigentlich ist es ja bereits so, das wenn das Wetter plötzlich anders ist - und auch weitere Faktoren hinzukommen - Mittags die Prognose angepasst wird.

Mach erst mal weiter wie gehabt, es gab momentan sehr viele Umbauten, lass uns erst noch mal Erfahrung sammeln.

Gruß Ralf

Hallo @All-Geier .. vielen Dank für die super netten Worte! Ich bekomme ja in der Regel immer nur mit wenn etwas “nicht funktioniert” :slight_smile:
Im Kern ist das eine Gute Überlegung den MDC “weicher” zu machen. Leider gibt es da nur ein Problem, der MDC ist eine Ausnahme und eine Art “Rettungsanker” bei schweren Problemen.
Darum ist er auch so restriktiv. Ein Tag an dem der MDC getriggert wurde, wird automatisch vom Lernen ausgeschlossen - aus dem oben genannten Gründen.
Das würde in letzter Konsequenz bedeuten, dass ein regelmäßigeres Triggern zu verzögerten Lernen führen würde.

Hallo @alteMade Danke!!!

Das ist ein sehr gute Punkt. Zur Einordnung, ein “Umbau” in dieser Größenordnung ist vergleichbar mit der Umstellung von Json zur Datenbank.. es können viele neue Edge-Cases auftreten und zu Problemen führen.
Eine Anmerkung, wenn das nun auch nicht unrsächlich damit zu tun hat.. der Einwand von Dir und einigen Anderen vor ein paar Wochen “Lieber etwas konservativer” hat sich ausgezahlt! - Danke noch mal!

Das ist ein Forum in dem deutsch geschrieben wird. Ganz offen Leute wie Du nerven bloß. Such Dir bitte ein anderes Forum in dem Du Dich austoben kannst.
Danke!

Bin ich auch immer noch dafür, lieber konservativer als zu optimistisch. Damit man mit den Daten auch gut arbeiten kann. Lieber habe ich persönlich etwas über als am Ende enttäuscht zu werden.

MDC trigger ja, aber es müssen ja tatsächlich harte Punkte erreicht werden. - >Energie oder “Himmelsverdeckung”

2 „Gefällt mir“